diff --git a/@l10n/ru/docs/advanced/hardfork-management.md b/@l10n/ru/docs/advanced/hardfork-management.md index c9298c8edf..0c9ffe8402 100644 --- a/@l10n/ru/docs/advanced/hardfork-management.md +++ b/@l10n/ru/docs/advanced/hardfork-management.md @@ -48,6 +48,8 @@ VIZ Ledger координирует обновления протокола че | 10 | Модель инфляции | | 11 | Изменения модели эмиссии | | 12 | Аварийное восстановление консенсуса (см. ниже) | +| 13 | Длина эпохи распределения (`chain_properties_hf13`) | +| 14 | Прогнозные рынки (Onix): 18 операций + 7 виртуальных, CPMM/LMSR, паримутюэль-расчёт, оракулы, споры, commit-reveal, lazy-пул; chain properties v5 | --- diff --git a/@l10n/ru/docs/consensus/emergency-consensus.md b/@l10n/ru/docs/consensus/emergency-consensus.md index 5fdf5ffbf9..b1177250b9 100644 --- a/@l10n/ru/docs/consensus/emergency-consensus.md +++ b/@l10n/ru/docs/consensus/emergency-consensus.md @@ -151,9 +151,9 @@ real_witness_slots >= CHAIN_MAX_VALIDATORS × 75% | Механизм | Поведение в экстренном режиме | |----------|-------------------------------| | `resync_from_lib()` | **Полностью пропускается** — извлечение блоков вблизи LIB в экстренном режиме приведёт к сбою | -| `stale_sync_check_task()` | Если голова мастера продвигается → сбросить таймер, пропустить восстановление; если голова последователя застряла → разрешить восстановление | +| `check_wedge_watchdog()` | **Отдельной ветки под экстренный режим нет.** Взводится только когда голова заморожена далеко ниже *подтверждённой* вершины сети (вторая по высоте голова среди установленных пиров), любое продвижение головы сбрасывает таймер. Только лог, если не выставлен `auto-resync-on-wedge = true` — тогда подтверждённый клин завершает ноду | | `handle_block()` (DLT, режим синхронизации, разрыв 0–2) | Обрабатывается как нормальный (не синхронизация) для предотвращения нарушения цикла производства | -| Обнаружение зависшей синхронизации снимков | Та же логика, что и проверка зависшей синхронизации | +| Обнаружение зависшей синхронизации снимков (`enable-stalled-sync-detection`, по умолчанию выключено) | **Именно здесь и живёт разделение мастер/последователь.** По таймауту читается `emergency_consensus_active`; если мы — экстренный мастер (держим экстренный ключ и комитет в расписании), соло-производство блоков нормально, и восстановление пропускается. Застрявшему последователю восстановление разрешено | Защита `resync_from_lib()` наиболее критична: в экстренном режиме LIB близок к HEAD. Извлечение блоков до LIB и сброс fork DB привели бы к тому, что блоки пиров из реальной сети связываются с повторно заполненным LIB, запуская переключение форка, извлечение ниже зафиксированного LIB и либо сбой, либо повреждение состояния. @@ -196,7 +196,7 @@ real_witness_slots >= CHAIN_MAX_VALIDATORS × 75% | 6 | `maybe_produce_block` (мастер) | Обходить синхронизацию, устаревание, участие; пропускать minority fork | | 7 | `maybe_produce_block` (последователь) | Сначала синхронизироваться; 21-блочная проверка изоляции | | 8 | `resync_from_lib` | **Полностью пропускать** в экстренном режиме | -| 9 | `stale_sync_check_task` | Пропустить если голова мастера продвигается; разрешить если последователь застрял | +| 9 | `check_wedge_watchdog` | Ветки под экстренный режим нет; от ложного взвода защищают подтверждение вершины и сброс при продвижении головы | | 10 | `handle_block` | Почти догнавшие блоки обрабатываются как нормальные в DLT-экстренном режиме | | 11 | `database::open` | Исправление расписания при запуске | | 12 | `validator_guard` | Не подавлять восстановление ключей в экстренном режиме | diff --git a/@l10n/ru/docs/governance/chain-properties.md b/@l10n/ru/docs/governance/chain-properties.md index 0358a42f28..1c2b1d2227 100644 --- a/@l10n/ru/docs/governance/chain-properties.md +++ b/@l10n/ru/docs/governance/chain-properties.md @@ -122,8 +122,28 @@ | `chain_properties_hf4` | 1 | HF4 | inflation_validator_percent, inflation_ratio_committee_vs_reward_fund, inflation_recalc_period | | `chain_properties_hf6` | 2 | HF6 | data_operations_cost_additional_bandwidth, validator_miss_penalty_percent, validator_miss_penalty_duration | | `chain_properties_hf9` | 3 | HF9 | create_invite_min_balance, committee_create_request_fee, create_paid_subscription_fee, account_on_sale_fee, subaccount_on_sale_fee, validator_declaration_fee, withdraw_intervals | +| `chain_properties_hf13` | 4 | HF13 | distribution_epoch_length | +| `chain_properties_pm` | 5 | HF14 | ~30 параметров прогнозных рынков + kill-switch `pm_commit_reveal_enabled`, `pm_lazy_pool_enabled` | -Для всех новых публикаций параметров валидатора используйте индекс версии 3 (`chain_properties_hf9`). +Для всех новых публикаций параметров валидатора используйте индекс версии **5** (`chain_properties_pm`). Индекс 4 — `chain_properties_hf13` (`distribution_epoch_length`). + +### Параметры прогнозных рынков (v5, HF14) {#pm-parameters} + +Все медиан-голосуемые; см. [Операции прогнозных рынков](../protocol/operations/prediction-markets.md). + +Все проценты PM — в bp (10000 = 100.00%), как прочие `*_percent`; промилле (‰) нигде нет. + +- **Оракул:** `pm_min_oracle_insurance`, `pm_max_oracle_fee_percent` (**единственный** governance-кэп на фи — на % оракула), `pm_oracle_registration_fee`, `pm_oracle_penalty_percent`, `pm_oracle_dispute_response_sec`, `pm_oracle_accept_window_sec` (по умолчанию 3600 = 1 ч — названный оракул должен принять или отклонить пендинг-рынок в течение этого окна; по истечении крон возвращает создателю сид-ликвидность, но **не** комиссию за создание, и аннулирует рынок → `pm_market_expired`). +- **Риск / покрытие** *(процент от объёма ставок рынка, 100 = 1.0×):* `pm_listing_min_coverage_percent` (250 = 2.5×) — рынки, чья страховка оракула покрывает меньше этой доли их объёма, скрыты из каталога `list_markets` по умолчанию (показываются через `show_risky`); `pm_betting_min_coverage_percent` (150 = 1.5×) — рекомендательный порог, публикуемый для клиентов, чтобы требовать явного подтверждения риска перед ставкой (не форсится on-chain; должен быть `≤ pm_listing_min_coverage_percent`). +- **Рынок:** `pm_min_liquidity`, `pm_market_creation_fee`, `pm_max_outcomes`, `pm_max_market_duration`. *(Агрегатного кэпа фи нет; creator/liquidity-фи без кэпа, самолимитируются; статический инвариант `сумма ≤ 100%`.)* +- **Batch / commit-reveal:** `pm_batch_epoch_blocks`, `pm_reveal_window_blocks`, `pm_min_batch_bet`, `pm_commit_no_reveal_penalty_percent`, `pm_commit_reveal_enabled`. +- **Споры:** `pm_dispute_fee`, `pm_dispute_grace_sec`, `pm_dispute_vote_period_sec`, `pm_dispute_auto_close_sec`, `pm_dispute_approve_min_percent`, `pm_no_contest_penalty_percent`, `pm_dispute_reward_multiplier` (bp-множитель, 10000 = 1×). +- **Time penalty:** `pm_default_time_penalty_percent`, `pm_max_time_penalty`. +- **Lazy-пул:** `pm_lazy_pool_enabled`, `pm_lazy_alloc_percent`, `pm_lazy_max_total_alloc_percent`, `pm_lazy_recall_step_percent`, `pm_lazy_lock_sec`, `pm_lazy_emergency_penalty_percent`, `pm_lazy_min_liquidity_fee_percent` (по умолчанию 200 = 2% — пул отказывается со-предоставлять ликвидность рынку, чей `liquidity_fee_percent` ниже этого порога вознаграждения). +- **Плечо (опц.):** `pm_leverage_enabled`, `pm_leverage_fund_percent`, `pm_leverage_max_per_position_bp`, `pm_leverage_max_position_ratio_percent`, `pm_leverage_min_market_liquidity`, `pm_leverage_safety_margin_percent`, `pm_leverage_max_slippage_percent`, `pm_leverage_m_factor_percent`, `pm_leverage_pool_profit_percent`, `pm_leverage_expiration_buffer_sec`, `pm_conversion_profit_cost_percent`. +- **Справедливость:** `pm_processing_cap_per_block`. + +Три флага `*_enabled` (`pm_commit_reveal_enabled`, `pm_lazy_pool_enabled`, `pm_leverage_enabled`) — живые kill-switch: медиана валидаторов может отключить commit-reveal, lazy-пул или плечо без нового хардфорка. --- diff --git a/@l10n/ru/docs/node/configuration.md b/@l10n/ru/docs/node/configuration.md index c4c5370a48..73fc1e5561 100644 --- a/@l10n/ru/docs/node/configuration.md +++ b/@l10n/ru/docs/node/configuration.md @@ -158,6 +158,11 @@ skip-virtual-ops = false # Разрешить производство при устаревшей цепочке (только для разработки/тестовой сети) enable-stale-production = false +# Отключить обнаружение minority fork (ТОЛЬКО для тестовой сети/форка одного оператора). +# В отличие от enable-stale-production, никогда не сбрасывается автоматически при +# здоровом участии. Никогда не включайте в реальной публичной сети. +disable-minority-fork-detection = false + # Минимальный процент участия для производства блоков (0–99) required-participation = 33 @@ -203,4 +208,4 @@ logger.p2p.appenders = p2p | `plugins/chain/plugin.hpp` | `shared-file-size`, `min-free-shared-file-size`, `inc-shared-file-size`, `block-num-check-free-size`, `single-write-thread`, `enable-plugins-on-push-transaction`, `read-wait-micro`, `max-read-wait-retries`, `write-wait-micro`, `max-write-wait-retries`, `skip-virtual-ops`, `clear-votes-before-block`, `track-account-range`, `history-whitelist-ops`, `history-blacklist-ops`, `history-start-block` | | `plugins/p2p/p2p_plugin.hpp` | `p2p-endpoint`, `p2p-max-connections`, `p2p-seed-node`, `checkpoint` | | `plugins/webserver/webserver_plugin.hpp` | `webserver-http-endpoint`, `webserver-ws-endpoint`, `webserver-thread-pool-size` | -| `plugins/validator/validator.hpp` | `enable-stale-production`, `required-participation`, `validator`, `private-key`, `emergency-private-key`, `fork-collision-timeout-blocks`, `ntp-server`, `ntp-request-interval`, `debug-block-production` | +| `plugins/validator/validator.hpp` | `enable-stale-production`, `disable-minority-fork-detection`, `required-participation`, `validator`, `private-key`, `emergency-private-key`, `fork-collision-timeout-blocks`, `ntp-server`, `ntp-request-interval`, `debug-block-production` | diff --git a/@l10n/ru/docs/node/validator-node.md b/@l10n/ru/docs/node/validator-node.md index da0cc4b192..7925729ea9 100644 --- a/@l10n/ru/docs/node/validator-node.md +++ b/@l10n/ru/docs/node/validator-node.md @@ -164,6 +164,9 @@ docker run -d \ ### Обнаружение minority fork Если форк-база данных узла показывает 21+ последовательных блоков только от собственных валидаторов, узел автоматически откатывается к LIB и выполняет повторную синхронизацию. Это позволяет обнаружить сетевую изоляцию. +> [!WARNING] Форки одного оператора +> На тестовой сети или форке основной сети, где **один оператор контролирует все валидаторы**, «21 блок подряд от нас» — это нормальное здоровое состояние, поэтому детектор бесконечно откатывается к LIB. `enable-stale-production = true` здесь **не помогает**: при участии ≥33% это переопределение автоматически снимается на каждом блоке. Используйте вместо него `disable-minority-fork-detection = true` — он обходит и стандартный, и DLT-путь обнаружения и никогда не сбрасывается автоматически. **Никогда не включайте его в реальной публичной сети** — это убирает защиту от изоляции. + ### Watchdog производства Если в течение 180 секунд (60 с для экстренного мастера) при активном `should_be_producing` не был произведён ни один блок, watchdog автоматически сбрасывает зависшие флаги (`minority_fork_recovering`, нагон P2P, синхронизация цепочки) и пытается возобновить производство. diff --git a/@l10n/ru/docs/plugins/prediction-market-api.md b/@l10n/ru/docs/plugins/prediction-market-api.md new file mode 100644 index 0000000000..7fa3ac25aa --- /dev/null +++ b/@l10n/ru/docs/plugins/prediction-market-api.md @@ -0,0 +1,192 @@ +# Плагин `prediction_market_api` + +Read-only JSON-RPC доступ к состоянию прогнозных рынков HF14 (рынки, ставки, оракулы, ликвидность, споры, lazy-пул, chain properties v5). Плагин возвращает сырые консенсус-объекты `pm_*` напрямую плюс несколько вычисляемых DTO. + +**Включение:** добавить `prediction_market_api` в список плагинов узла (в `vizd` зарегистрирован по умолчанию). Зависит от `chain` + `json_rpc`. Все list-методы пагинируются через `from` (пропуск) и `limit` (`≤ 1000`). + +## Методы + +### Рынки + +| Метод | Аргументы | Возврат | +|-------|-----------|---------| +| `get_market` | `market_id` | `pm_market_object` | +| `list_markets` | `status, from, limit, [show_risky]` | `pm_market_object[]` | +| `list_markets_by_oracle` | `oracle, from, limit` | `pm_market_object[]` | +| `list_markets_by_creator` | `creator, from, limit` | `pm_market_object[]` | +| `get_market_outcomes` | `market_id` | `pm_outcome_object[]` | +| `get_market_weight_sums` | `market_id` | `pm_market_weight_sums` (вычисляемый) | +| `get_market_bets` | `market_id, from, limit` | `pm_bet_object[]` | +| `get_market_liquidity` | `market_id, from, limit` | `pm_liquidity_object[]` | +| `get_market_full` | `market_id, [account]` | `pm_market_full` (вычисляемый) | + +`status` для `list_markets`: `-1` удалён, `0` ожидание, `1` активен, `2` закрыт, `3` разрешён. По +умолчанию `list_markets` скрывает недострахованные рынки (страховка оракула < **2.5×** объёма ставок); +`show_risky = true` показывает их (рынки только скрываются, ставки on-chain всегда разрешены). + +`get_market_full` — **обогащённое представление за один вызов** для экрана деталей рынка: возвращает рынок + исходы + суммы весов + оракула (с надёжностью) + распарсенные метаданные и — если передан необязательный `account` — ставки, плечевые позиции и LP этого аккаунта **на этом рынке**. Экономит тонкому клиенту несколько round-trip'ов. + +### Метаданные рынка (парсятся off-chain) + +Каждый рынок несёт свободную, консенсус-непрозрачную JSON-строку `metadata`. Плагин парсит индексируемые +ключи (категория / подкатегория / теги / запрещённые юрисдикции) в `pm_market_meta_object` — **только для +отображения/индексации, не консенсус**. + +| Метод | Аргументы | Возврат | +|-------|-----------|---------| +| `get_market_meta` | `market_id` | `pm_market_meta_object` (или ошибка, если нет) | +| `list_markets_by_category` | `category, from, limit, [jurisdiction], [subcategory], [tag], [sort]` | `pm_market_meta_object[]` | +| `get_market_categories` | — | `pm_market_categories` (вычисляемый) | + +`list_markets_by_category` исключает рынки, чьи `banned_jurisdictions` содержат необязательный ISO-код +`jurisdiction` (регулируемый клиент передаёт свою юрисдикцию, чтобы получить только допустимые рынки). +Необязательные `subcategory` (точное совпадение) и `tag` (членство в CSV) сужают набор; `sort` ∈ `newest` +(id рынка по убыв., по умолч.) · `oldest` · `volume` (`bets_sum` по убыв.) · `expiration` +(`betting_expiration` по возр.). `get_market_categories` возвращает живую таксономию — счётчики по +категориям / подкатегориям плюс топ-20 горячих тегов (`jurisdiction-*` исключены) — агрегированную по +проиндексированным сейчас рынкам, чтобы браузинг-UI строил свои фильтр-чипы без захардкоженной таксономии. +Объект: `market`, `category`, `subcategory`, `tags` (через запятую), `banned_jurisdictions` (ISO через +запятую; пусто = разрешено везде), `expiry` (пруна после закрытия окна спора + TTL). + +### Позиции и оракулы + +| Метод | Аргументы | Возврат | +|-------|-----------|---------| +| `get_account_positions` | `account, from, limit` | `pm_position[]` (ставка + `expected_payout`) | +| `get_account_leverage_positions` | `account, from, limit` | `pm_leverage_position_object[]` | +| `get_market_leverage_positions` | `market_id, from, limit` | `pm_leverage_position_object[]` | +| `get_creator_ban` | `account` | `pm_creator_ban_object` (или ошибка, если нет) | +| `get_oracle` | `owner` | `pm_oracle` (объект + `reliability_score`) | +| `list_oracles` | `from, limit` | `pm_oracle_object[]` | + +### Превью плеча (Boost) + +Read-only котировки, вызывающие **ту же внутриузловую математику маржи**, что и эвалуаторы, так что превью совпадает с тем, что вычислила бы соответствующая операция `pm_leverage_*` на головном блоке. Это неконсенсусные оценки (резервы двигаются между чтением и бродкастом — всегда отправляйте on-chain защиту от проскальзывания). + +| Метод | Аргументы | Возврат | +|-------|-----------|---------| +| `get_leverage_quote` | `market_id, outcome_index, collateral` | `pm_leverage_quote` (вычисляемый) | +| `get_leverage_close_preview` | `position_id` | `pm_leverage_close_preview` (вычисляемый) | +| `get_leverage_convert_preview` | `position_id` | `pm_leverage_convert_preview` (вычисляемый) | + +`get_leverage_quote` зеркалит `pm_leverage_open`: возвращает максимальный платёжеспособный заём и итоговое максимальное плечо, кэпы пула/позиции, до 12 стопов слайдера (каждый с токенами, порогом, текущей и худшей стоимостью отмены) и — когда плечо невозможно — `available = false` со списком `failed_constraints[]`. `get_leverage_close_preview` / `get_leverage_convert_preview` зеркалят `pm_leverage_close` / `pm_leverage_convert` при текущих резервах (стоимость отмены, обязательство пула, что получает беттер, закрываемость/конвертируемость и комиссия конвертации при текущей медиане `pm_conversion_profit_cost_percent`). + +> Выплата каждому беттору — виртуальная операция `pm_payout` (стейк, side/outcome, итог; `0` при +> проигрыше); закрытие плечевой позиции — `pm_leverage_resolve` (`outcome_index`, `won`, `leverage`). +> Обе видны в `account_history`; сами объекты позиций — через методы выше. + +### Споры, lazy-пул, governance + +| Метод | Аргументы | Возврат | +|-------|-----------|---------| +| `get_dispute` | `market_id` | `pm_dispute_object` | +| `get_dispute_votes` | `market_id` | `pm_dispute_votes` (голоса + живой подсчёт) | +| `get_lazy_pool` | — | `pm_lazy_pool_object` | +| `get_lazy_deposit` | `account` | `pm_lazy_deposit_object` | +| `get_lazy_allocations` | `from, limit` | `pm_lazy_allocation_object[]` | +| `get_market_lazy_allocation` | `market_id` | `pm_lazy_allocation_object` (или ошибка, если нет) | +| `get_pm_chain_properties` | — | `chain_properties_pm` (медиана, v5) | + +`get_lazy_allocations` перечисляет записи аллокаций lazy-пула по рынкам (для дашборда пула); `get_market_lazy_allocation` берёт запись для конкретного рынка. Штрафные штампы оракула отдельного метода не требуют — они идут на `pm_oracle_object` (`penalty_stamps`, `last_penalty_stamp_time`) через `get_oracle`. + +### Графики — kline / история весов + +Тайм-серия для построения графика изменения веса каждого исхода. Плагин добавляет точку **каждый раз, когда веса исходов рынка меняются** — ставка, отмена, ликвидация, batch-settle, открытие плеча или расчёт плеча — как таймстампированный снимок паримутюэль-веса (сумма ставок) по каждому исходу. Это **неконсенсусное** состояние плагина (хранится в chainbase, undo/redo-безопасно, не входит в хеш состояния); история копится с момента первого включения плагина на узле. + +**Хранение:** kline-история пруна́ется **вместе с метаданными рынка**, по тому же расписанию — `result_expiration` + grace спора + `pmm-ttl-days` (по умолч. 7). Полный график рынка доступен на протяжении его жизни и в окне хранения после расчёта, затем оба индекса очищаются (для очень длинной истории — частями за несколько блоков), чтобы хранилище узла оставалось ограниченным. + +| Метод | Аргументы | Возврат | +|-------|-----------|---------| +| `get_market_kline` | `market_id, [from], [limit]` | `pm_kline[]` (по возрастанию `seq`) | + +Пагинация — **отступ от новейших** (намеренно простая для тонких клиентов): `from` — сколько **новейших** точек пропустить, `limit ≤ 1000` — размер страницы. +- `(market_id, 0, 1000)` → последние ≤ 1000 изменений. +- `(market_id, 1000, 1000)` → предыдущие 1000 (страницей раньше) — повторяй с `from += 1000`, чтобы дозагружать более старую историю. + +График: x = `timestamp` (unix-секунды), по одной линии на исход `i` с y = `weights[i]` (или нормированно `weights[i] / Σweights` — вменённая вероятность). + +## Вычисляемые DTO + +- **`pm_position`** — ставка + `expected_payout` (выплата, если сторона победит, или реализованная после расчёта; байт-в-байт повторяет `settle_market`), `market_status`, `resolved_outcome`. +- **`pm_oracle`** — объект оракула + `reliability_score` (bp `[0..10000]`, неконсенсусная эвристика: смесь доли успешных разрешений и доли выигранных споров минус штраф за баны). +- **`pm_market_weight_sums`** — `bets_sum`/`weight_sum` по сторонам/исходам (веса считаются сканом ставок, т.к. не хранятся). +- **`pm_kline`** — одна точка графика: `seq` (uint32, 0-based, монотонный индекс изменения по рынку), `timestamp` (unix-секунды, x), `reason` (uint8: 0 ставка, 1 отмена, 2 ликвидация, 3 batch settle, 4 открытие плеча, 5 расчёт плеча), `bets_sum` (всего поставлено), `weights[]` (вес по каждому исходу, y; индекс = outcome_index). +- **`pm_dispute_votes`** — голоса + подсчёт finalize. Старые поля (вес = `|vote_percent|`, не стейк): `uphold_weight`/`challenge_weight`/`total_weight`, `challenger_leads` (≥ `pm_dispute_approve_min_percent`), `proposed_outcome`. **Точная stake-взвешенная проекция (зеркалит `pm_dispute_finalize`; все `*_shares` в vesting-shares = `effective_vesting_shares` + стейк lazy-пула→shares):** `participation_shares` (Σ веса проголосовавших), `electorate_shares` (`total_vesting_shares` + NAV пула→shares), `quorum_required_shares`, `quorum_percent_bp` (кворум в bp, 10000 = 100.00%), `quorum_reached` (bool), `oracle_defense_shares`/`change_shares`, `outcome_change_shares[]` (по исходам), `expected_uphold` (останется ли решение оракула), `expected_outcome` (какой исход будет выставлен при резолюции сейчас), `expected_consensus_strength_bp`. Проекция совпадает с тем, что крон применит на `voting_end_time` при текущих голосах (голоса изменяемы до этого момента). + +**`pm_market_full`** — обогащённое представление рынка за один вызов (`oracle`/`meta` = `null`, когда +отсутствуют; массивы `my_*` пусты, если не передан аргумент `account`): +``` +{ market: pm_market_object, + outcomes: pm_outcome_object[], // пусто для бинарных рынков + weight_sums: pm_market_weight_sums, + oracle: pm_oracle | null, + meta: pm_market_meta_object | null, + my_positions: pm_position[], // ставки аккаунта на ЭТОМ рынке + my_leverage_positions: pm_leverage_position_object[], + my_liquidity: pm_liquidity_object[] } +``` + +**`pm_leverage_quote`** — превью открытия плеча (из `pm::leverage::*`, та же математика, что и в эвалуаторе): +``` +{ available: bool, outcome_index, collateral, + max_loan, max_leverage_x100, // 100 = 1.00× + pool_free_amount, fund_available, per_position_cap, market_position_cap, + pool_profit_percent, safety_margin_percent, max_slippage_percent, m_factor_percent, + expiration_buffer_sec, auto_close_time, // betting_expiration − buffer + stops: [ { leverage_x100, loan, total_bet, expected_tokens, pool_profit, + liquidation_threshold, current_cancel_value, worst_case_cancel_value } ], + failed_constraints: [ { constraint, reason } ] } // заполнено, когда !available +``` +**`pm_leverage_close_preview`** — `{ position_id, outcome_index, cancel_value, pool_obligation, bettor_receives, collateral, loan, pool_profit_charge, closeable: bool, loss_vs_collateral, loss_percent_bp }`. +**`pm_leverage_convert_preview`** — `{ position_id, outcome_index, cancel_value, pool_obligation, current_profit, conversion_profit_cost_percent, conversion_fee, total_user_payment, convertible: bool }`. + +**`pm_market_categories`** — браузинг-таксономия с живыми счётчиками: +``` +{ categories: [ { category, count, subcategories: [ { subcategory, count } ] } ], // сортировка по count убыв. + hot_tags: [ { tag, count } ] } // топ-20 (jurisdiction-* исключены) +``` + +## Пример + +Последние 1000 точек графика для рынка `42`, затем предыдущие 1000: +```bash +# новейшая страница +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_market_kline",[42,0,1000]]}' http://127.0.0.1:8090 +# страницей старше +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_market_kline",[42,1000,1000]]}' http://127.0.0.1:8090 +``` + +Тонкий клиент (дозагрузка старой истории при прокрутке назад) — каждая точка в серию `{ x: unixtime, y: weight }` по исходам: +```js +async function call(method, params) { + const r = await fetch('http://127.0.0.1:8090', { method: 'POST', + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'call', + params: ['prediction_market_api', method, params] }) }); + return (await r.json()).result; +} + +// Страницы по 1000 от новейших назад, пока не наберём `want` точек (или не кончится история). +async function loadKline(marketId, want = 3000) { + const points = []; + for (let from = 0; points.length < want; from += 1000) { + const page = await call('get_market_kline', [marketId, from, 1000]); + if (!page.length) break; // дошли до начала истории + points.unshift(...page); // страницы по возрастанию; старые — в начало + if (page.length < 1000) break; + } + return points; +} + +// По одной серии {x,y} на исход — напрямую в любую библиотеку графиков. +function toSeries(points, outcomeCount) { + const series = Array.from({ length: outcomeCount }, () => []); + for (const p of points) + for (let i = 0; i < outcomeCount; i++) + series[i].push({ x: p.timestamp, y: Number(p.weights[i]) }); + return series; +} +``` + +См. [Операции прогнозных рынков](../protocol/operations/prediction-markets.md) и [Chain Properties](../governance/chain-properties.md#pm-parameters). diff --git a/@l10n/ru/docs/plugins/validator.md b/@l10n/ru/docs/plugins/validator.md index 7666743682..566b700431 100644 --- a/@l10n/ru/docs/plugins/validator.md +++ b/@l10n/ru/docs/plugins/validator.md @@ -24,6 +24,7 @@ chain::plugin, p2p::p2p_plugin, snapshot::snapshot_plugin | `private-key` | — | WIF-ключ(и) для подписи; может повторяться | | `emergency-private-key` | — | WIF-ключ для экстренного консенсуса; автоматически добавляет `CHAIN_EMERGENCY_VALIDATOR_ACCOUNT` в набор валидаторов | | `enable-stale-production` | `false` | Обход проверок участия и синхронизации (только для тестовой сети / восстановления сети) | +| `disable-minority-fork-detection` | `false` | Полностью пропустить обнаружение minority fork (только для тестовой сети/форка одного оператора). Никогда не сбрасывается автоматически при здоровом участии — см. [Обнаружение minority fork](#обнаружение-minority-fork) | | `required-participation` | `3300` | Минимальное участие валидаторов в **базисных пунктах** (3300 = 33%) | | `fork-collision-timeout-blocks` | `21` | Количество последовательных отсрочек при коллизии форков перед принудительным производством (один полный раунд валидаторов) | @@ -116,7 +117,8 @@ chain::plugin, p2p::p2p_plugin, snapshot::snapshot_plugin Перед каждой попыткой производства (после проверок безопасности HF12) плагин просматривает последние 21 блок в `fork_db`. Если все 21 были произведены собственными настроенными валидаторами узла — узел изолирован на minority fork. - **Действие по умолчанию:** Вызов `p2p().resync_from_lib()` — откат блоков к LIB, сброс fork DB, повторная инициация синхронизации P2P, переподключение к начальным узлам. Возвращает `minority_fork`. -- **С `enable-stale-production=true`:** Запись предупреждения, продолжение производства. +- **С `enable-stale-production=true`:** Запись предупреждения, продолжение производства. **Примечание:** при участии ≥33% это переопределение автоматически снимается на каждом блоке, поэтому на форке одного оператора оно *не* останавливает детектор — используйте `disable-minority-fork-detection`. +- **С `disable-minority-fork-detection=true`:** И стандартный, и DLT-путь обнаружения полностью пропускаются, а флаг никогда не сбрасывается автоматически. Для тестовой сети/форков одного оператора, где «21 блок подряд от нас» — это нормальное устойчивое состояние. **Никогда не включайте в реальной публичной сети** — это убирает защиту от изоляции. - **Пропускается при:** Активном экстренном консенсусе (блоки комитета всегда соответствовали бы нашему настроенному набору). В экстренном режиме вместо него используется специфичная для DLT проверка изоляции ведомого. --- @@ -217,7 +219,7 @@ validator[skip_flags=0x0 catching_up=0 head=#79881136 last_prod=45s_ago minority | `no_private_key` | В конфиге отсутствует `private-key` для ключа подписи, зарегистрированного в блокчейне | | `low_participation` | Участие сети < 33%; проверьте подключение к пирам или установите `enable-stale-production=true` | | `fork_collision` | Конкурирующий блок на следующей высоте; ждать разрешения по весу голосов или таймаута 21 отсрочки | -| `minority_fork` | Изолирован; плагин автоматически пересинхронизируется с LIB | +| `minority_fork` | Изолирован; плагин автоматически пересинхронизируется с LIB. На форке одного оператора зацикливается — установите `disable-minority-fork-detection=true` | | Watchdog срабатывает повторно | Флаг синхронизации или нагона завис; watchdog сбросит автоматически при продвижении головы | | Логи `SLOT-HIJACK` | Экстренный мастер обнулил наш ключ; восстановите через `validator_update_operation` | diff --git a/@l10n/ru/docs/prediction-markets/concepts-analysis.md b/@l10n/ru/docs/prediction-markets/concepts-analysis.md new file mode 100644 index 0000000000..13bb38ed28 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/concepts-analysis.md @@ -0,0 +1,244 @@ +--- +title: Анализ концептов — Onix против 90 концептов прогнозных рынков +description: Как живая on-chain реализация на VIZ (протокол Onix, клиент Forecaster) ложится на 90 теоретических концептов прогнозных рынков — что решено, имманентно, не нужно или в роадмапе. +--- + +# Анализ концептов — Onix против 90 теоретических концептов + +> **Forecaster** — это тонкий клиент к on-chain прогнозному рынку VIZ, слой доступа, позволяющий людям +> со всего мира участвовать, подписывая операции `pm_*` (см. [обзор раздела](./)). +> **Onix** — протокол, к которому он обращается. Эта страница отображает каждый из **90 концептов +> прогнозных рынков PM Atlas** на **то, как живая on-chain реализация на VIZ его закрывает**, и **нужен +> ли он** для этой архитектуры. +> +> Опорные документы: [whitepaper](./whitepaper), [спецификация](./specification), +> [воркфлоу и диспуты](./workflows). + +## Легенда + +| Метка | Значение | +|------|---------| +| ✅ **Решено** | Дизайн Onix напрямую решает/обрабатывает это | +| ⚪ **Имманентно** | Свойство, которое Onix демонстрирует/наследует by construction (доп. работа не нужна) | +| ➖ **Не нужно** | Архитектурно избыточно в Onix | +| 🟡 **Частично / Roadmap** | Частично решено сегодня; остальное — в роадмапе VIZ | +| 🏛 **Слой клиента** | Решается на уровне юрисдикционного клиента, не протокола | +| 🔴 **Открыто / Риск** | Остаётся живой проблемой; не решено полностью | + +Главное структурное отличие от всех остальных платформ: **принципал LP структурно гарантирован (победителям платят только из проигранных ставок), ценообразование — CPMM для бинарных и LMSR-softmax + parimutuel-сеттлмент для мульти, и нет ордербука.** Большая часть концептов «ликвидность и трейдинг» существует именно для управления инвентарным риском маркет-мейкера CLOB/LMSR — и поэтому **не применима** к Onix, где нет инвентарь-несущего мейкера. + +> **Актуализация on-chain (HF14 / live).** Эта таблица изначально писалась по whitepaper/спецификации. Ряд пунктов, помеченных тогда как *roadmap*, теперь **реализованы как consensus-операции** и проверены в `consensus_sim`: +> - **Батч-аукционы + commit-reveal ставки** — `pm_commit_bet` / `pm_reveal_bet` / `pm_batch_settle`, per-market `allow_batch` / `allow_instant_bet`, median kill-switch `pm_commit_reveal_enabled`. **Только бинарные** (мульти форсит `allow_instant_bet` — LMSR-батча пока нет). +> - **Опциональный leverage-сабсистем** — `pm_leverage_open/close/convert`, фондируется из Lazy Pool, kill-switch `pm_leverage_enabled` (по умолчанию off). Прямо закрывает **Position Collateralization (#30)**. +> - **Сам Lazy Pool** — синглтон, auto-allocation, MasterChef-учёт, leverage-займы, graduated recall. +> - Поле рынка **`endogeneity_tier`**; on-chain **баны создателей** (`pm_creator_ban_object`); расширенные settlement-vop (`pm_payout` на каждого беттора, `pm_leverage_resolve`, `pm_market_accepted`, `pm_auto_payout`) + методы плагина. +> - **Новое относительно спеки:** **стейк в lazy pool учитывается как вес в голосовании** — и в PM-диспутах, *и* в голосовании за заявки DAO-комитета (конвертация в vesting-shares, гейт HF14). +> +> **Намеренно НЕ делаем:** commit-reveal голосование в *диспутах* — диспуты комитета это **открытые публичные слушания by design** (голоса остаются публичными через `pm_dispute_vote`, и бюллетень можно менять до закрытия). **Остаётся roadmap:** автоматические экзогенные data-оракулы. Строки и таблица смягчений ниже приведены к этому live-состоянию. + +--- + +## 1. Информационная теория (Information Theory) + +| # | Концепт | Вердикт | Как Forecaster-на-VIZ это обрабатывает / нужно ли | +|---|---------|---------|--------------------------------------------------| +| 1 | **Brier Score** | ⚪ Имманентно | Не механизм протокола, а *метрика* оценки рынков Onix. На VIZ каждая ставка/резолюция — consensus-validated событие, значит истории цен и исходы полностью on-chain → Brier-скоринг платформы (и оракулов) считается кем угодно. Нужен как вход для аналитики/репутации, не как ядро. | +| 2 | **Calibration** | ⚪ Имманентно | Цены Onix — настоящие вероятности (CPMM `P(A)=reserve_b/(reserve_a+reserve_b)`, LMSR-softmax суммируется в 1). Калибровка — emergent-свойство для *измерения*, косвенно улучшается time-penalty (давит no-info ставки в последнюю секунду) и глубокой LP-ликвидностью. Протокол её не навязывает. | +| 3 | **Credibility Markets** | 🟡 Частично | Bonded-oracle + 14-метричная репутация + composite trust score — это по сути credibility-рынок для *резолверов*. Ставка репутацией против исхода — нативна. Общий продукт «стейкай репутацию на заявлениях» — возможная сборка на слое клиента, не ядро. | +| 4 | **Distribution Markets** | 🟡 Roadmap | Onix Multi (3–10 дискретных исходов) аппроксимирует распределение через бакеты. Настоящие непрерывные distribution-рынки (CDF/скаляр) **не** в скоупе сегодня; нужна скалярная операция исхода. Примыкает к roadmap-пункту «category-level AMMs». | +| 5 | **Endogeneity** | 🟡 Частично (смягчено) | Риск, что рынок меняет то, что предсказывает. Категориально-специфичен (эконом-данные чисто, политика/соц рискованно). Смягчается **живым полем `endogeneity_tier`** (тег оракула 1/2/3) и **опциональными commit-reveal/батч ставками (теперь on-chain)**, которые не дают публичной цене «течь» во время приёма (канал «термостата»); экзогенная резолюция через automated data oracles — **всё ещё roadmap**. См. [раздел Смягчения](#смягчения-для-семейства-рефлексивности). | +| 6 | **Forecasting Accuracy** | ⚪ Имманентно | Вся ценностная гипотеза. Onix улучшает её косвенно: risk-free LP → глубже книги → меньше слиппедж → больше информированного участия → лучше цены. Точность — это выход для измерения, не фича. | +| 7 | **Info Finance** | ⚪ Имманентно | Onix *и есть* инфо-финансовый инструмент: consensus-level операции превращают информацию в priced, settle-able позиции. Миграция на VIZ делает информационный слой цензуроустойчивым и композируемым. | +| 8 | **Information Aggregation** | ✅ Решено | Основная функция. CPMM/LMSR агрегируют разрозненные ставки в единую вероятность; глубокая risk-free LP-ликвидность — именно тот рычаг, которым Onix заставляет агрегацию работать (флайвил §7.3 whitepaper). | +| 9 | **Information Asymmetry** | 🟡 Частично | Дизайн Onix (parimutuel/CPMM) означает, что информированные трейдеры извлекают прибыль из *других проигравших бетторов*, а не из LP — поэтому асимметрия не банкротит ликвидность (в отличие от CLOB/LMSR-мейкеров). **Опциональные commit-reveal + батч ставки теперь live (бинарные):** закоммиченные ставки сеттлятся по единой батч-цене, убирая утечку направления через mempool; per-market `allow_batch` + median kill-switch оставляют это опциональным. | +| 10 | **Legibility** | ✅ Решено | Каждое финансовое действие — consensus-validated VIZ-операция с audit trail `market_log` (до/после резервов). Полностью читаемо/аудируемо любым узлом — строго легибельнее централизованного бэкенда или непрозрачного CLOB. Новые settlement-vop (**`pm_payout`** на каждого беттора, **`pm_leverage_resolve`**, **`pm_market_accepted`**) + методы плагина делают per-bettor исходы и leverage-резолюции напрямую запрашиваемыми. | +| 11 | **Longshot Bias** | 🟡 Частично | CPMM/LMSR всё ещё могут давать favorite-longshot bias из поведения бетторов; Onix не правит это напрямую. Time penalty и глубокая ликвидность гасят искажение, но это поведенческий выход, не устранён. | +| 12 | **Noise Decomposition** | ⚪ Имманентно | Аналитическая линза, не фича протокола. On-chain ряды цен/объёмов на VIZ делают разложение «сигнал-шум» возможным для аналитиков. В ядре не нужно. | +| 13 | **Nowcasting** | ⚪ Имманентно | Цены Onix обновляются на каждую ставку (~3с блоки VIZ), давая real-time nowcast-оценки. Имманентно любому живому AMM-рынку; доп. механизма нет. | +| 14 | **Price Discovery** | ✅ Решено | CPMM и LMSR-softmax — непрерывные движки price discovery; когерентность цен (`Σ price = 1`) держится *by construction*, без слоя арбитража/split-merge. | +| 15 | **Probability Infrastructure** | ✅ Решено | Это по сути тезис Onix на VIZ: prediction markets как **first-class consensus операции** (`pm_*`), не смарт-контракты — base-layer вероятностный примитив. Прямая цель миграции. | +| 16 | **Superforecasting** | ⚪ Имманентно | Концепт индивидуального скилла; Onix награждает точных бетторов через payout losers→winners. Трансфер позиций + репутация могут поддержать идентичность суперфорекастера, но это черта участника, не логика протокола. | +| 17 | **Wisdom of Crowds** | ✅ Решено | Механизм, который Onix монетизирует. Risk-free LP снижает барьер, чтобы участвовала бóльшая часть толпы, заостряя агрегат. Ядро дизайнерского обоснования. | +| 18 | **Yes Bias** | 🟡 Частично | Поведенческий перекос к «Yes». Симметричный CPMM и profit-only time penalty структурно не благоприятствуют Yes, но и не правят человеческий bias. Смягчается глубиной ликвидности; вопрос измерения. | + +--- + +## 2. Дизайн механизмов (Mechanism Design) + +| # | Концепт | Вердикт | Как Forecaster-на-VIZ это обрабатывает / нужно ли | +|---|---------|---------|--------------------------------------------------| +| 19 | **Binary Contracts** | ✅ Решено | Onix Binary = CPMM (`x·y=k`) на двух исходах. Доказательство AM-GM гарантирует `reserve_a+reserve_b ≥ L`, значит принципал LP покрыт. Основной тип рынка. | +| 20 | **Combinatorial Prediction Markets** | ➖ Не нужно (сегодня) | LMSR естественно раскладывается по комбинаторным пространствам, но Onix Multi ограничен 3–10 *независимыми* исходами и намеренно опускает CTF split/merge. Комбинаторные/conditional-бандлы явно вне скоупа; для модели LP-гарантии не требуются. | +| 21 | **Incentive Compatibility** | ✅ Решено | LMSR наследует IC от log scoring rule (truth-telling доминантна). Onix добавляет согласование стимулов через bonded-оракулов (insurance > прибыли манипуляции), сеттлмент losers-fund-winners и time-weighted LP-награды. | +| 22 | **Keynesian Beauty Contest** | ✅ Решено (рынок) / ⚪ (слой диспутов — принято by design) | KBC — патология *относительного/peer-скоринга*. **Рыночный** слой Onix платит бетторам против внешней истины (parimutuel), значит он структурно анти-KBC — ты зарабатываешь, *отклоняясь* от цены толпы, когда она неправа. Единственная экспозиция KBC — **stake-weighted голосование комитета в диспуте** (peer-механизм). **Решение: commit-reveal в диспутах внедряться НЕ будет** — диспут комитета это **открытые публичные слушания**, и доверие к ДАО держится на максимально прозрачном разрешении споров; сокрытие голосов это доверие подорвало бы. Остаточный риск KBC принят и структурно мал: голосующим **не платят** за совпадение с большинством (нет bandwagon-бонуса), а **бюллетень можно менять** до закрытия (честные апдейты на новые доводы ожидаемы, а не подавляются). Участники пула также **с правом голоса** (стейк lazy pool → vesting-shares, HF14). См. [раздел Смягчения](#смягчения-для-семейства-рефлексивности). | +| 23 | **LMSR** | ✅ Решено (ключевая инновация) | Файл концепта отмечает, что LMSR *провалился на бинарных* (перманентный убыток на границе 0/1). Ответ Onix: **CPMM для бинарных, а LMSR только для мульти, где мейкер НЕ контрагент** — parimutuel-сеттлмент платит победителям из проигравших, поэтому LMSR-субсидия никогда не под риском (`макс. убыток LP = 0` против `b·ln(N)`). Центральный ход дизайна. | +| 24 | **LOX (Log-Odds Excess Lateness)** | ➖ Не нужно | Специализированная метрика скоринга/запаздывания. Onix вместо этого использует **квадратичный time penalty на прибыль** для стимулов поздних ставок — проще, на этапе сеттлмента. LOX-скоринг не часть модели. | +| 25 | **Market Manipulation** | 🟡 Частично | Bonded-оракул (бонд > прибыли манипуляции), DPoS-валидируемые операции и арбитраж комитета повышают стоимость манипуляции. Манипуляция ценой большими ставками ограничена глубиной и — на **батч/commit-reveal рынках (теперь live)** — единой клиринговой ценой, нейтрализующей speed-снайпинг («налог снайпера»); активный surveillance — всё ещё roadmap. | +| 26 | **Market Scoring Rules** | ✅ Решено | Onix Multi — реализация market scoring rule (LMSR), переиспользованная с parimutuel-сеттлментом. Используется напрямую. | +| 27 | **Multi-Outcome Markets** | ✅ Решено | Onix Multi обрабатывает N=3–10 через LMSR-softmax + parimutuel payout, с `b = S/ln(N)`. First-class тип рынка. | +| 28 | **Parimutuel Markets** | ✅ Решено (фундамент) | Сеттлмент в *обоих* типах рынков — parimutuel: проигранные ставки формируют `winners_pool`, распределяемый по доле токенов. Именно это делает гарантию LP структурной, а не страховой. | +| 29 | **Peer Prediction** | ➖ Не нужно | Схемы truth-telling без ground-truth. Onix опирается на bonded-оракулов + комитет, не на peer-prediction скоринг. Могло бы помочь резолюции субъективных рынков, но не используется. | +| 30 | **Position Collateralization** | ✅ Решено (+ опц. leverage, live) | По умолчанию каждая ставка полностью предоплачена (вся сумма входит в резервы; нет комиссий при ставке) — тотальная коллатерализация by construction. Теперь Onix **также** реализует «next level» концепта: **опциональный leverage-сабсистем** (`pm_leverage_open/close/convert`, kill-switch `pm_leverage_enabled`, по умолчанию off). Маржа — это **займ из Lazy Pool** (без эмиссии токенов — zero-sum сохранён), поэтому позиция остаётся полностью обеспеченной *с точки зрения системы*. Бинарный «jump risk», ломающий liquidation-движки CLOB (по этому концепту), решается **ликвидацией по pre-bet резервам**: opposing-bet / settlement force-close возвращает `min(cancel_value, obligation) ≥ loan`, так что пул получает заём + проценты; **единственный** ограниченный путь bad-debt — same-side `pm_cancel_bet` (Case B). Каскад ликвидации намеренно **не** гейтится kill-switch'ем, поэтому выключение leverage никогда не снимает защиту с открытых позиций. | +| 31 | **Proper Scoring Rules** | ✅ Решено | LMSR — cost-function дуал log proper scoring rule; Onix Multi наследует его truthful-elicitation свойство. | +| 32 | **Reflexivity** | 🟡 Частично (смягчено) | Родитель endogeneity. Смягчается тем же набором — **commit-reveal/батч ставки + `endogeneity_tier` теперь live**, экзогенная резолюция всё ещё roadmap — **плюс on-chain баны создателей** (`pm_creator_ban_object`) для вредной рефлексивности (рынки убийств/«hit», пропаганда-рынки, создающие «конституенцию за исход»); сам *список* запрещённых категорий остаётся на слое клиента. Глубокая risk-free LP-ликвидность также повышает стоимость newsworthy-манипуляции ценой. См. [раздел Смягчения](#смягчения-для-семейства-рефлексивности). | + +--- + +## 3. Ликвидность и трейдинг (Liquidity & Trading) + +> **Заголовок:** в Onix **нет ордербука и нет инвентарь-несущего маркет-мейкера**. Большой класс этих концептов существует именно для управления инвентарным риском CLOB/мейкера, поэтому **не применим** к Onix. + +| # | Концепт | Вердикт | Как Forecaster-на-VIZ это обрабатывает / нужно ли | +|---|---------|---------|--------------------------------------------------| +| 33 | **Adverse Selection** | ✅ Решено (переосмыслено) | Классическая проблема (информированный поток банкротит мейкера) **не может обанкротить LP Onix**: победителям платят из проигравших, не из принципала LP (гарантия AM-GM / parimutuel). Информированные извлекают из других *бетторов*, не из LP. Устраняет ключевой режим отказа LP. | +| 34 | **Arbitrage** | ⚪ Имманентно | Внутрирыночный арбитраж не нужен: когерентность (`Σ price = 1`) держится by construction в CPMM и LMSR-softmax. Слой split/merge-арбитража не нужен. | +| 35 | **Batched Auctions** | ✅ Решено (опц., live) | Реализовано как **per-market uniform-price батч** (`mode=1` ставки + commit-reveal → `pm_batch_settle` на каждой эпохе `pm_batch_epoch_blocks`); AMM двигает только **нетто-остаток**, поэтому все same-side филлы клирятся по одной цене, а speed-снайпинг («налог снайпера») нейтрализуется. Per-market `allow_batch` + median kill-switch `pm_commit_reveal_enabled`; **только бинарные** сегодня (мульти форсит `allow_instant_bet`). Инвариант LP `Σreserve ≥ L` не тронут — каждый батч это один валидный CPMM-переход. | +| 36 | **Bid-Ask Spread** | ➖ Не нужно | Нет ордербука → нет котируемого спреда. «Стоимость торговли» проявляется как слиппедж CPMM/LMSR, зависящий от глубины, не от спредов мейкера. Концепт не мапится. | +| 37 | **Bonding Trades** | ⚪ Имманентно | Ставки *и есть* bonded-сделки: капитал внесён в резервы и высвобождается только при резолюции (или через отмену/трансфер). Нативное поведение. | +| 38 | **Continuous Double Auction** | ➖ Не нужно | CDA — модель CLOB, которую Onix явно отвергает в пользу AMM. Не используется. | +| 39 | **Covariance Markets** | ➖ Не нужно | Торговля корреляцией событий требует комбинаторной/conditional структуры, которую Onix опускает. Вне скоупа. | +| 40 | **Cross-Platform Arbitrage** | 🟡 Частично | Цены Onix могут расходиться с Polymarket/Kalshi; арбитраж между платформами возможен, но внешний для протокола. Открытый API VIZ + headless-клиент делают данные доступными; нативного моста нет. | +| 41 | **Execution Quality** | ✅ Решено (переосмыслено) | Нет частичных филлов/очереди. Качество исполнения = детерминированный слиппедж + опц. `min_tokens`/`min_return`-гард, валидируемый на консенсусе. Предсказуемо by construction. | +| 42 | **Gap Risk** | ✅ Решено (для LP) | Gap-риск (резкий скачок к 0/1, выносящий мейкера) — режим отказа, который структурная гарантия LP Onix устраняет: LP никогда не держит терминальный риск проигравшей стороны. Бетторы по-прежнему несут свой риск исхода (как и задумано). | +| 43 | **Hedging** | 🟡 Частично | Бетторы могут хеджироваться встречными позициями, трансфером (`pm_transfer_position`), отменой ставки (если разрешена) через reverse CPMM и теперь **опциональным leverage** (`pm_leverage_open/convert`) для капиталоэффективного оффсета. Нативных мульти-leg деривативов нет; базовый + leveraged хедж возможен. | +| 44 | **Implied Correlation** | ➖ Не нужно | Требует мульти-событийных/комбинаторных рынков, которые Onix опускает. Вне скоупа. | +| 45 | **Insider Trading** | 🟡 Частично / 🏛 Клиент | Протокол не может детектить инсайд; смягчается time penalty (поздние инфо-ставки дают меньше прибыли) и bonded-резолюцией. KYC/surveillance для полиции инсайдеров — ответственность **слоя клиента** (регулируемые клиенты). | +| 46 | **Kelly Criterion** | ⚪ Имманентно | Стратегия сайзинга беттора, не фича протокола. Onix отдаёт чистые вероятности и полную коллатерализацию, так что Kelly-сайзинг считается участниками; новый **опциональный leverage** позволяет беттору отыгрывать fractional-Kelly эдж с маржой (фондируется из пула, ограничено ликвидацией). Ядро не вовлечено сверх предоставления примитивов. | +| 47 | **Liquidity Fragmentation** | 🟡 Roadmap | Per-market пулы фрагментируют ликвидность сегодня. Топ-приоритет роадмапа — **shared/category-level AMM-пулы** — архитектурный фикс. Lazy Pool уже мьютуализирует *депозиты* между рынками. | +| 48 | **Liquidity Provision** | ✅ Решено (ключевой дифференциатор) | Risk-free LP — заголовок: принципал структурно гарантирован, time-weighted награды, Lazy Pool auto-allocation + MasterChef-учёт. Решает проблему «LP теряют деньги», ради которой и создан протокол. | +| 49 | **Market Making** | ✅ Решено (переосмыслено) | Активный мейкер не нужен — AMM + LP-пул *и есть* мейкер, и он не несёт инвентарного риска. «Market making» схлопывается в пассивное risk-free предоставление ликвидности. | +| 50 | **Minimum Viable Liquidity** | ✅ Решено | Принудительный пол: мин. начальная ликвидность 100 VIZ; Lazy Pool авто-засевает каждый новый рынок `free_balance × allocation_%`. MVL структурно бутстрапится, не оставлен на случай. | +| 51 | **Order Book** | ➖ Не нужно | Onix на AMM; ордербука нет by design. | +| 52 | **Orderflow Arbitrage** | ➖ Не нужно | Нет ордербука / нет PFOF-роутинга потока → неприменимо. | +| 53 | **Relative Value Trading** | ➖ Не нужно | Кросс-инструментный RV требует коррелированных/комбинаторных рынков, которые Onix опускает. Вне скоупа. | +| 54 | **Retail Flow** | ✅ Решено (переосмыслено) | В CLOB ритейл-поток субсидирует убытки мейкера от toxic flow. В Onix мейкера, которого надо защищать, нет — ритейл и информированные бетторы платят в один parimutuel-пул; LP безразличен. Напряжение «ритейл vs toxic» растворяется на слое LP. | +| 55 | **Semantic Tick Size** | ➖ Не нужно | Гранулярность тика — концепт CLOB. Цены Onix — непрерывные AMM-функции; точность — фикс. mVIZ-единица (1/1000). Дизайн тика не нужен. | +| 56 | **Temporal Arbitrage** | 🟡 Частично | Ставка раньше vs позже несёт разный риск; **time penalty на прибыль** в Onix — именно тот механизм, что закладывает в цену запаздывание, демпфируя «жди-определённости» арбитраж. Не устранён, но явно дестимулирован. | +| 57 | **Time Arbitrage** | 🟡 Частично | То же семейство, что #56 — эксплуатация тайминга информации. Квадратичный time penalty + ~3с такт блоков снижают, но не убирают edge. Учтён по дизайн-намерению. | +| 58 | **Toxic Flow** | ✅ Решено (для LP) | Определяющая проблема CLOB/LMSR (снайпер выкупает книгу по 10¢ на исходе-99¢, мейкер ест 80¢) **не бьёт по LP Onix** — payouts из ставок проигравших, а субсидия LP возвращается безусловно. Toxic flow здесь просто значит, что информированные бетторы выигрывают parimutuel-пул, как и задумано. Крупный структурный выигрыш. | +| 59 | **Wash Trading** | 🟡 Частично / 🏛 Клиент | Отсутствие комиссий при ставке убирает один стимул wash, но накрутка объёма возможна; трансферы — чистая переуступка (там фарм комиссий невозможен). Детекция/surveillance — забота клиента + роадмапа. | + +--- + +## 4. Оракул и резолюция (Oracle & Resolution) + +| # | Концепт | Вердикт | Как Forecaster-на-VIZ это обрабатывает / нужно ли | +|---|---------|---------|--------------------------------------------------| +| 60 | **Corruption Value Multiple (CVM)** | ✅ Решено (принципом дизайна) | Явный security-инвариант протокола: **бонд страховки оракула должен превышать потенциальную прибыль манипуляции**. Risk factor (insurance/bets) питает composite trust score. CVM — напрямую обоснование бондинга. | +| 61 | **Dispute Resolution** | ✅ Решено | Полная система: 12ч grace, `dispute_fee`, обязательный ответ оракула, per-market резолвер (`dispute_mode==0` stake-weighted голосование комитета / `==1` именованный резолвер), slashing страховки, 3-исходные no-contest диспуты, 14-дневный auto-close против заморозки, on-chain баны создателя/оракула. Одна из наиболее проработанных частей. **Дополнение HF14:** депозитчики lazy pool сохраняют вес голоса в диспуте (NAV пула → vesting-shares, добавляется к `effective_vesting_shares`). | +| 62 | **Oracle Design** | ✅ Решено | Bonded-модель: рег-комиссия, ≥5000 VIZ страховки, явный акцепт, резолюция с доказательствами, 14-метричная репутация, freshness decay, механика банов. Ядровая подсистема. | +| 63 | **Resolution Criteria** | 🟡 Частично / 🏛 Клиент | Вопрос/критерии рынка в `url`/описании (custom_json, display-only). Протокол навязывает *процесс* (кто резолвит, диспуты), но не *качество* критерия — двусмысленные критерии — ответственность создателя/клиента, полиция ретроспективно через диспуты + **on-chain баны создателя** (`pm_creator_ban_object`, теперь live). | +| 64 | **Self-Resolving Markets** | ➖ Не нужно (сегодня) | Резолюция Onix — oracle-driven, не алгоритмическая. Automated data oracles (Chainlink-style фиды) — высокоприоритетный roadmap для объективных рынков, что аппроксимировало бы self-resolution. | +| 65 | **UMA Protocol** | ➖ Не нужно (заменено) | Оптимистичный оракул UMA (у Polymarket) функционально заменён bonded-оракулом Onix + диспут-моделью комитета VIZ. Та же проблема, нативное VIZ-решение — без внешней зависимости от оракула. | + +--- + +## 5. Governance и решения (Governance & Decisions) + +| # | Концепт | Вердикт | Как Forecaster-на-VIZ это обрабатывает / нужно ли | +|---|---------|---------|--------------------------------------------------| +| 66 | **Attention Markets** | ➖ Не нужно | Торговля вниманием/виральностью — отдельный продукт; Onix фокусируется на резолюции событий. Возможна категория на слое клиента, не ядро. | +| 67 | **Conditional Tokens** | ➖ Не нужно (явно) | Whitepaper §7.2 доказывает, что CTF split/merge **архитектурно не нужен** — когерентность цен математическая, не навязанная токенами. *Единственную* полезную фичу CTF (трансфер позиций) реализуют нативно как `pm_transfer_position` с шифрованными memo. Намеренно опущено. | +| 68 | **Decision Markets** | 🟡 Возможно | Onix Multi мог бы выразить decision-рынки, но conditional-структура «если-политика-то-метрика» не нативна (нет conditional tokens). Собираемо на слое клиента; не ядровый примитив. | +| 69 | **Futarchy** | 🟡 Возможно (клиент) | Файл концепта: футархия = decision-рынки на conditional futures. У Onix нет нативных conditional-рынков, поэтому полная футархия в ядре не поддержана. Stake-weighted комитет VIZ уже управляет *параметрами*; governance-by-market — конструкция клиента/роадмапа. | +| 70 | **Hyperstition Markets** | ➖ Не нужно (намеренно) | Рефлексивность-как-фича (координировать, не предсказывать). Это *дизайн-выбор, а не баг для фикса*: требование Onix, чтобы исход был **внешне верифицируем bonded-оракулом**, структурно исключает hyperstition-рынки по умолчанию. Может существовать как отдельный «coordination market» продукт на слое клиента с milestone-резолюцией, но не ядровая цель. См. [раздел Смягчения](#смягчения-для-семейства-рефлексивности). | +| 71 | **Impact Markets** | ➖ Не нужно | Retrospective-funding/impact-сертификаты — отдельный домен. Возможна категория на слое клиента; не ядро. | +| 72 | **No-Loss Prediction Markets** | 🟡 Смежно | Onix не no-loss для *бетторов* (проигравшие теряют ставки — это финансирует победителей). Но он **no-loss для *LP*** (принципал гарантирован). Yield-funded no-loss вариант (стейкаешь yield, принципал возвращается) — другая модель; no-loss на стороне LP уже реализован. | +| 73 | **Opportunity Markets** | ⚪ Имманентно (смежно) | **Opportunity-cost protection** Lazy Pool (graduated recall, active-market penalty, fault stamps) напрямую адресует opportunity-cost капитала — хотя «opportunity markets» как категория продукта вне скоупа. | + +--- + +## 6. Бизнес и платформы (Business & Platforms) + +| # | Концепт | Вердикт | Как Forecaster-на-VIZ это обрабатывает / нужно ли | +|---|---------|---------|--------------------------------------------------| +| 74 | **AI agents** | 🟡 Roadmap | Headless-клиент + открытые VIZ-операции делают программных агентов (бетторы, LP, автоматические оракулы) простыми. AI-driven ликвидность/резолюция — естественное расширение, ещё не специфицировано. | +| 75 | **Cross-subsidization** | ⚪ Имманентно | Lazy Pool кросс-субсидирует ликвидность по многим рынкам с одного депозита; MasterChef `reward_per_share` шарит fee-yield. Кросс-субсидирование встроено в экономику пула. | +| 76 | **Demand markets** | ➖ Не нужно | Рынки замера/агрегации спроса — категория продукта; не ядровый примитив Onix. Слой клиента. | +| 77 | **Distribution moat** | 🟡 Стратегия | Моат Onix — risk-free LP-yield + VIZ-нативная инфраструктура (флайвил). Дистрибуция (Telegram WebApp сегодня → headless web-клиент) — go-to-market, частично решается платформонезависимостью после миграции. | +| 78 | **Election markets** | 🏛 Клиент | Поддержаны как обычные бинарные/мульти рынки; их *легальность* — вопрос юрисдикционного клиента (whitelisted оракулы, фильтры категорий). Протокол нейтрален. | +| 79 | **Event contracts** | ⚪ Имманентно | Каждый рынок Onix *и есть* event contract. Регуляторная классификация — вопрос клиента/права, не логика протокола. | +| 80 | **Federal preemption** | 🏛 Клиент (N/A протоколу) | Файл концепта: зависит от того, «swaps» ли US event contracts. VIZ DLT — **инфраструктура, не оператор** (whitepaper §6.2) — как Bitcoin это леджер. Правовые обязательства привязаны к клиентам, не к консенсусу. Не забота протокола. | +| 81 | **Long-tail markets** | ✅ Решено | Именно ниша, которую включает bounded-loss LMSR — и Onix делает её *risk-free* для засева через Lazy Pool auto-allocation + пол ликвидности. Жизнеспособность long-tail — ядровый аргумент продажи. | +| 82 | **Market structure** | ✅ Решено (определено) | Onix определяет чёткую структуру: AMM-ценообразование, parimutuel-сеттлмент, bonded-оракулы, DPoS-управляемые параметры, consensus-level операции. Связная, новая рыночная структура против CLOB-платформ. | +| 83 | **Market surveillance** | 🟡 Roadmap / 🏛 Клиент | Полный on-chain audit trail (`market_log`, каждая операция consensus-validated) делает surveillance *возможным* для любого. Активный surveillance/энфорсмент — забота клиента + роадмапа. | +| 84 | **Network effects** | 🟡 Стратегия | Флайвил (risk-free LP → глубина → бетторы → комиссии → ещё LP) — задуманный сетевой эффект. Shared liquidity pools (roadmap) усиливают его. Go-to-market, не механика протокола. | +| 85 | **Parlays** | ➖ Не нужно | Многоногие комбо-ставки требуют conditional/комбинаторной структуры, которую Onix опускает. Вне скоупа (возможна сборка клиента поверх независимых рынков). | +| 86 | **Platform competition** | 🟡 Стратегия | Конкурирует уникальным «passive yield without impermanent loss» против Polymarket/Kalshi (таблица сравнения whitepaper §7.1). Стратегическое позиционирование, не логика протокола. | +| 87 | **Polymarket** | ⚪ Референс | Главный бенчмарк. Onix отличается по всем осям: CPMM/LMSR vs CLOB, ноль LP-риска vs инвентарный риск, нативные операции vs контракты Polygon, bonded-оракул vs UMA, без CTF. Используется для сравнения, не перенят. | +| 88 | **Regulatory arbitrage** | 🏛 Клиент | Модель юрисдикционного клиента: каждый регион строит свой compliant (или permissionless) клиент на нейтральных рельсах VIZ. Регуляторное позиционирование живёт целиком на слое клиента. | +| 89 | **Regulatory classification** | 🏛 Клиент | Являются ли рынки swaps/gaming/securities — решается per-юрисдикция на слое клиента; протокол классификационно-нейтрален (те же `pm_*` для permissionless и регулируемых клиентов). Не забота протокола. | + +--- + +## Итог — что реально меняет дизайн Onix-на-VIZ + +**Решено структурно (ключевые выигрыши):** +- LP-сторонние **adverse selection, toxic flow, gap risk, impermanent loss, инвентарный риск мейкера** → все устранены, т.к. победителям платят только из проигранных ставок, а принципал LP возвращается безусловно (доказательство AM-GM для CPMM; parimutuel-сеттлмент для LMSR). +- **Провал LMSR на бинарных рынках** → обойдён использованием CPMM для бинарных и ограничением LMSR мульти-рынками, где мейкер не контрагент. +- **Liquidity provision, minimum viable liquidity, жизнеспособность long-tail** → risk-free LP + Lazy Pool auto-allocation. +- **Oracle design, dispute resolution, CVM** → bonded-оракул + 14-метричная репутация + stake-weighted диспуты комитета. +- **Price discovery, arbitrage, conditional tokens** → когерентность цен математическая (`Σ price = 1`), значит не нужны ордербук, split/merge, внутренний слой арбитража. + +**Не нужно / намеренно опущено:** ордербук, CDA, bid-ask spread, semantic tick size, orderflow arbitrage, CTF split/merge, комбинаторные/covariance/correlation/relative-value/parlay рынки, UMA, peer prediction, LOX. + +**Вытолкнуто на слой юрисдикционного клиента:** federal preemption, regulatory classification/arbitrage, легальность election-рынков, KYC/энфорсмент инсайда, surveillance. + +**Реализовано с момента написания таблицы (теперь live on-chain, HF14):** опциональные батч-аукционы + commit-reveal ставки (бинарные), опциональный leverage-сабсистем (position collateralization), Lazy Pool, `endogeneity_tier`, on-chain баны создателей, settlement-vop на беттора/leverage + методы плагина, и вес стейка lazy pool в PM-диспутах + голосовании за заявки DAO-комитета. + +**В роадмапе VIZ (частично сегодня):** shared/category liquidity pools (фикс фрагментации), automated data oracles (→ self-resolving объективные рынки), distribution markets, AI agents. *(Прим.: commit-reveal в **диспутах** в этом списке **нет** — он намеренно отклонён; слушания диспутов остаются публичными. Commit-reveal для **ставок** уже live.)* + +**Остаётся открытым / поведенческое (смягчено, не устранено):** longshot/yes bias, манипуляция ценой через глубину, кросс-платформенный арбитраж. У семейства рефлексивности (endogeneity, reflexivity, KBC, hyperstition) есть конкретный план смягчения — см. ниже. + +--- + +## Смягчения для семейства рефлексивности + +Endogeneity, reflexivity, Keynesian beauty contest (KBC) и hyperstition — это **одна корневая проблема на разных слоях**: рынок/цена влияет на исход, который он измеряет. Один небольшой набор примитивов адресует все четыре. + +### Карта корневой причины + +| Слой | Концепт | Канал | +|------|---------|-------| +| Уровень форекастера | **Keynesian Beauty Contest** | стадность к видимому консенсусу в *относительном/peer-скоринге* | +| Уровень рынка | **Endogeneity** | *существование/видимость* рынка меняет поведение (категориально) | +| Уровень рынка | **Reflexivity** | общая обратная связь цена↔реальность; манипуляция-как-пропаганда | +| По дизайну | **Hyperstition** | рефлексивность используется *намеренно* для координации исхода | + +### Примитивы смягчения + +| Примитив | Статус | Что чинит | Заметки | +|----------|--------|-----------|---------| +| **Commit-reveal голосование в диспутах** | **отклонено (делать НЕ будем)** | KBC | Диспут комитета это **открытые публичные слушания**: `pm_dispute_vote` — публичный бюллетень, и **остаётся таким by design** — доверие к ДАО держится на прозрачном разрешении споров. Бюллетень **можно менять** до закрытия (повтор перезаписывает), чтобы голосующие честно обновляли решение на новые доводы; остаточный KBC принят (за совпадение с большинством не платят). | +| **Commit-reveal ставки (батчем)** | **live (опц., бинарные)** | endogeneity, reflexivity, инфо-асимметрия | `pm_commit_bet`/`pm_reveal_bet`/`pm_batch_settle` скрывают направление/размер in-flight потока, чтобы публичная цена не «текла» во время приёма (убивает канал термостата). Сеттлится единой ценой **батчем** (см. ниже). | +| **Поле рынка `endogeneity_tier`** | **живое поле** | endogeneity | Оракул тегирует tier 1 (эконом-данные — чисто), 2 (спорт/расписание), 3 (политика/соц — рискованно); UI показывает уровень рефлексивного риска; клиенты могут ограничивать tier-3. | +| **Экзогенная резолюция (automated data oracles)** | roadmap (high) | endogeneity, reflexivity | Резолюция привязана к внешнему фиду (BLS/ФРС/спорт-API), на который рынок не влияет → чистый термометр. | +| **Список запрещённых категорий + бан создателя** | **бан создателя live on-chain**; список — слой клиента | вредная рефлексивность, hyperstition | Блокировать рынки, где YES создаёт «конституенцию за исход» (рынки убийств/«hit»/терактов, пропаганда-рынки). Через on-chain бан создателя (`pm_creator_ban_object`) + фильтр категорий клиента. | +| **Глубокая risk-free LP-ликвидность** | ядро сегодня | манипуляционная рефлексивность | Флайвил делает книгу глубокой, поэтому двигать цену ради «newsworthy» манипулированного заголовка — дорого. | + +### KBC: почему рыночный слой уже безопасен + +KBC — патология **относительного скоринга** (платят за близость к peers → стадность к peers). Рыночный слой Onix платит бетторам против **внешней истины через parimutuel-сеттлмент** — тебя награждают за *отклонение* от неправильной цены толпы, а не за совпадение. Значит слой бетторов структурно анти-KBC. Единственный относительный/peer-механизм в протоколе — **stake-weighted голосование комитета в диспуте**, и остаточный риск KBC там **принят by design** — диспут оставлен открытыми публичными слушаниями (без commit-reveal), потому что доверие к ДАО держится на прозрачном разрешении споров; риск ограничивает другое — голосующим не платят за совпадение с большинством, а бюллетень можно менять по мере поступления доводов. + +### Commit-reveal против инварианта CPMM `a·b=k` + +CPMM **path-dependent** (токены зависят от резервов в момент исполнения), поэтому commit-reveal *нельзя* делать ставка-за-ставкой против живой кривой — порядок раскрытия вернул бы MEV и слил бы цену. Фикс (и почему §8.3 ставит commit-reveal рядом с batch-auction): **перестать обновлять кривую per-bet; обновлять её раз в эпоху через uniform-price батч-сеттлмент.** + +1. **Commit:** прислать `hash(side, amount, salt, min_tokens)` и заэскроить `amount`. +2. **Reveal:** раскрыть `(side, amount, salt)`; раскрытия собираются, но **не применяются** до конца эпохи (видеть чужие reveal бесполезно — своё уже закоммичено). +3. **Сеттлмент раз:** встречный поток (`A_in` vs `B_in`) неттится между бетторами по единой клиринговой цене `p*`; только **нетто-остаток** двигает AMM, поэтому `k` пересчитывается **один раз**. Все A-филлы получают `p*`, все B-филлы — `1−p*` → нет внутрибатчевого преимущества по порядку. +4. **Флор `min_tokens`:** поскольку цена невидима в момент commit, per-bet флор по токенам обязателен; если `tokens < min_tokens` на сеттлменте, ставка отклоняется и возвращается из эскроу. +5. **Анти-griefing:** нераскрытие → штраф из эскроу в LP-fee/DAO-пул, убивая атаку «закоммить опциональность, раскрой только победителей». + +**Гарантия LP не тронута:** каждый батч-сеттлмент — валидный CPMM-переход, значит AM-GM `reserve_a + reserve_b ≥ L` держится. Меняется только *гранулярность* обновления кривой (per-bet → per-epoch). Onix Multi аналогично через агрегированную LMSR cost-функцию. + +**Фазировка:** (1) сначала uniform-price батч-аукционы — уже дёшево убивают ordering-MEV/фронт-раннинг; (2) поверх — commit-reveal сокрытие — добавляет in-flight конфиденциальность, включается выборочно для tier-3 (endogeneity-чувствительных) рынков. **И (1), и (2) теперь реализованы on-chain для бинарных рынков** (мульти всё ещё форсит instant-ставки — LMSR-батча пока нет). + +### Изменение вердиктов + +| Концепт | Было | Стало | +|---------|------|-------| +| Keynesian Beauty Contest | 🔴 Открыто | ✅ рынок / ⚪ диспут — голоса публичны **by design** (без commit-reveal; бюллетень изменяем, голосующим не платят, участники пула с правом голоса) | +| Endogeneity | 🔴 Открыто | 🟡 смягчено — `endogeneity_tier` + commit-reveal/батч **live**; экзогенные оракулы roadmap | +| Reflexivity | 🔴 Открыто | 🟡 смягчено — бан создателя **live on-chain**; список категорий — слой клиента | +| Hyperstition | 🔴 Открыто | ➖ исключено по дизайну (опц. продукт клиента) | diff --git a/@l10n/ru/docs/prediction-markets/early-exit-deferred-claim.md b/@l10n/ru/docs/prediction-markets/early-exit-deferred-claim.md new file mode 100644 index 0000000000..7604e86c78 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/early-exit-deferred-claim.md @@ -0,0 +1,139 @@ +# Deferred claim при раннем выходе (F1 / #300) + +Статус: дизайн залочен (владелец 2026-08-08), реализация в работе на ветке `pm`. + +## Проблема + +Рынок — гибрид: **кривая** CPMM (binary) / LMSR (multi) для входа и раннего выхода, +и **pari-mutuel** сеттлмент для позиций, додержанных до резолва. Любой round-trip через +кривую (купить, потом продать до сеттлмента) реализует торговый P&L против глубины кривой — +против LP — ровно как impermanent loss в Uniswap. Но дизайн обещает LP **защиту принципала** +(только комиссии, без IL). Эти два требования в конфликте. + +Два code-пути выходят против кривой на бинарном рынке: + +- **Плечо** (`liquidate_position` / `pm_leverage_close`): всегда; force-закрывается на + сеттлменте; усилено займом пула. +- **Обычный cancel ставки** (`cancel_bet`, F2 curve-priced refund): в окне ставок. + +Оба роутят `residual = stake − curve_refund` в `forfeit_pool` (знаковый). Когда ранний выход +*прибылен* (`curve_refund > stake`), `forfeit_pool` уходит в **минус**. На сеттле +`winners_pool = losers_sum − fees + forfeit_pool`; если прибыль плеча/раннего выхода +перекрывает проигравшие стейки, `winners_pool < 0`, полом кладётся в 0, а недостача +(`uncovered`, F1) чарджится с принципала LP — либо минтится, когда принципал LP исчерпан. +Достижимо: доказано gate-respecting CPMM-симуляцией (односторонняя накачка, `uncovered = 7316`); +replay-корпус Бабина попал в это в 1163/1988 пар. + +Корень: **curve-priced выход платит значение bonding-кривой, не ограниченное проигравшим +пулом**, а сеттл платит pari-mutuel. Разрыв падает на LP. + +## Модель (залочена) + +Ранние выходы больше не извлекают curve-стоимость из LP. Вместо этого выход записывает +**outcome-contingent deferred claim**, финансируемый на сеттле из **ограниченной доли +проигравшего пула**. + +### Записывается при выходе +`{ position_id, kind (bet|leverage), chosen_outcome, claim_amount, exit_time }`. + +### Обычный cancel ставки +- **Принципал возвращается сразу, безусловно**: `refund = min(curve_refund, stake)` + (свои деньги, не заёмные). Cancel может срезать убыток или выйти в ноль, но никогда не + реализует curve-прибыль в момент cancel'а. +- **Хвост прибыли** `max(curve_refund − stake, 0)` → deferred claim на выбранный исход. +- **Depth-normalized pricing (аудит #1-C):** сплит cap/tail переоценивается по глубине кривой + *на входе* ставки, а не по текущей. `pm_bet_object.entry_liquidity` записывает + `liquidity_sum` в момент, когда CPMM-ставка попала в кривую (instant `place_bet` и batch fill). + На cancel оба резерва масштабируются на `entry_liquidity / liquidity_sum` (mirror-of-buy на + отмасштабированных резервах). Поскольку ставки держат `k` инвариантным, а liquidity-опы + масштабируют `k` на `f²` и `liquidity_sum` на `f`, `sqrt(k_entry / k_now) == L_entry / L_now` + **точно** → детерминированно, без sqrt (нет целочисленного sqrt). Результат **клампится к + реальному `curve_refund`**, так что нормализация может только *уменьшить* выплату, никогда не + поднять. Это убивает вектор self-liquidity-инфляции (ставка → собственный `add_liquidity` + раздувает глубину → больший `curve_refund` → cancel минтит больший хвост → вывод возвращает + ликвидность целиком = гарантированная cash-neutral прибыль), не открывая shrink-сторону. + Фолбэк на легаси-ценообразование, когда `entry_liquidity` отсутствует (ставки до поля). + +### Leverage close / liquidate +- **Залог НЕ возвращается отдельно** — это first-loss margin для пула. Пул возвращает своё + обязательство (`loan·(1+R) + funding`) из `cv` первым; если `cv < obligation`, залог закрывает + разрыв. +- **Остаток** `max(cv − obligation, 0)` → deferred claim на выбранный исход. +- Плечо, таким образом, — **leveraged directional bet**, а не harvest волатильности: вы в плюсе + только если ваш исход выиграл и в ведре есть место; неверный исход теряет залог. +- NB: теперь два разных предиката: **solvency** (`cv ≥ obligation`, управляет возвратом займа) vs + **outcome-win** (управляет правом на claim). Позиция может быть solvent, но на проигравшем + исходе → пул сделан целым, claim = 0. + +### Сеттлмент +1. `bucket = pm_early_exit_reward_cap_percent × losers_sum / 10000` (default 33%). +2. Собрать deferred claims **только на выигравшем исходе** (claims на проигравшем → 0). +3. Заплатить их **FIFO по `exit_time`** (первым вышел — первым оплачен), пока ведро не опустеет; + без per-position cap (владелец 2026-08-08: FIFO-порядок + общее ведро = граница). Claim, + который остаток ведра не может покрыть полностью, оплачивается частично; остаток не платится + (haircut). +4. **Неиспользованное ведро возвращается в winners' pool** — удержанные выигравшие ставки делят + его pari-mutuel. + +### Гарантии +``` +paid_claims ≤ bucket = cap · losers_sum +winners_pool = losers_sum − fees − paid_claims + honest_forfeits + ≥ (1 − cap) · losers_sum − fees ≥ 0 (cap < 100%) +``` +- `uncovered` **невозможен by construction**; без минта; **принципал LP не трогается**; lazy pool + не несёт leverage IL. +- **Проигравший исход никогда не в плюсе** (требование владельца). +- Удержанные победители получают `≥ (1 − cap)` проигравшего пула плюс любое неиспользованное + ведро. +- Ранний выход — это **ограниченная, contingent скидка** (≤ cap, FIFO) против додержания до + резолва (полная pari-mutuel доля) → нет арбитража против додержания; осознанная скидка за + ликвидность. + +## Chain-параметр + +`pm_early_exit_reward_cap_percent` (uint16, bp, default **3300** = 33% от `losers_sum`). +Median-voted параметр валидатора; `validate()` баундит `≤ 10000`. Добавлен в +`chain_properties_pm` + FC_REFLECT + `calc_median` (DONE, single-TU verified). + +## Touchpoints реализации (нода) + +- [x] chain-параметр `pm_early_exit_reward_cap_percent` (struct/validate/reflect/median). +- [ ] объект `pm_deferred_claim_object` (+ индекс по market, по exit_time) — space 30, дописать + в конец enum `object_type` (snapshot-safe, как `pm_lazy_withdraw_request`). +- [ ] `cancel_bet`: вернуть `min(curve_refund, stake)`, записать profit-tail claim; перестать + роутить отрицательный residual в `forfeit_pool`. +- [ ] `liquidate_position` / `pm_leverage_close`: пул забирает obligation, записать + `cv − obligation` claim, пометить выбранный исход; убрать immediate `bettor_received`; + прекратить отрицательный `forfeit_pool`. +- [ ] сеттлмент (`settle_market`): после force-close посчитать `bucket`, оплатить claims + выигравшего исхода FIFO по exit_time, остаток → winners' pool; убрать путь charge + `uncovered`/F1 в `settle_liquidity` (LP больше это не поглощает). +- [ ] снапшот: включить `pm_deferred_claim_object` в allowlist (+ import handler). +- [x] virtual op `pm_early_exit_claim_paid` (account, market, kind, outcome, `claimed`, `paid`) — + дописана в конец варианта `operation` (op-id стабильны), FC_REFLECT'нута, эмитится в + distribution-loop сеттлмента рядом с `adjust_balance`, и роутится в account_history + раннего выходящего (visitor impacted-accounts в `account_history`). `claimed` vs `paid` + показывает любой haircut от исчерпания ведра. `adjust_balance` один не оставляет следа в + истории — это закрывает разрыв. +- [x] read API: `get_deferred_claims(market, [from=0], [limit=100])` — FIFO-порядок выхода через + `by_claim_market`; пусто на settled рынке (claims consumed). Plugin-only (клиенты зовут через + rawApi/JSON-RPC), как `get_lazy_withdraw_requests`; без wallet-wiring. + +## Client / lib / docs follow-ups +- viz-js-lib / viz-php-lib / viz-python-lib: новый chain-параметр в v5 chain_properties_pm + (serialization lock-step, byte-verify), любой новый read-метод / vop. +- Forecaster: notices + описания операций (плечо = directional, ранний выход = ограниченная + скидка), показывать pending deferred claim на позициях. +- WebVIZWallet: те же обновления описаний операций, если показываются. +- Научная статья: `early-exit choice` с математикой (regular vs leverage-from-lazy-pool, + validator-set reward cap). + +## Отвергнутые альтернативы (почему) +- Размазать `uncovered` по всем LP / минтить (status quo) — ломает обещание принципала LP. +- Локализовать только в lazy pool — пул может быть исчерпан; всё равно аппроксимация; cancel'ы + протекают. +- Капнуть выигрыш на выходе — на момент выхода не знаешь `losers_sum`; deferral это убирает. +- Full-AMM сеттлмент — отказывается от pari-mutuel тезиса VIZ. +Deferred outcome-contingent claim — единственный вариант, дающий **жёсткую** гарантию LP, оставаясь +pari-mutuel. diff --git a/@l10n/ru/docs/prediction-markets/guides/active-lp.md b/@l10n/ru/docs/prediction-markets/guides/active-lp.md new file mode 100644 index 0000000000..94edba74ae --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/active-lp.md @@ -0,0 +1,42 @@ +--- +title: "Активный LP — прямая ликвидность рынка" +description: "Прямой LP задаёт глубину кривой конкретного рынка, зарабатывает на комиссиях и штрафах поздних ставок, и principal-protected: вывод price-neutral, а settle_liquidity возвращает принципал безусловно. Плечо LP не касается." +--- + +# Активный LP: прямая ликвидность рынка + +Вы даёте рынку глубину: кладёте VIZ в кривую конкретного рынка, чтобы ставки не двигали цену слишком резко. За это вы получаете долю комиссий. Ключевое отличие от «маркет-мейкера-банкира»: на VIZ прямая ликвидность **защищена по принципалу** — вы не можете уйти в минус от исхода. + +## Главное в двух абзацах + +Ваш капитал (`pm_add_liquidity`) встаёт в кривую рынка и определяет, насколько плавно ставки двигают цену. Чем глубже пул, тем комфортнее крупным игрокам и тем больше объём — а значит и комиссий. Вы зарабатываете **процент со ставок** (комиссия рынка) плюс долю **штрафов поздних ставок** (anti-sniping penalty), которые копятся в пользу LP. + +Ваш принципал не зависит от того, кто выиграл. Вывод (`pm_withdraw_liquidity`) — **price-neutral**: пропорциональный шринк резервов возвращает ваш принципал, не двигая кривую (round-trip не меняет цену). А на сеттлменте рынка `settle_liquidity` возвращает каждому LP его принципал **безусловно**, плюс бонус (комиссии, невыплаченные победителям остатки, penalty-pool). LP = principal-protected + доход с комиссий — и доход с комиссий реализуется **только** на сеттле. + +## Что происходит по шагам + +**Вносите ликвидность.** `pm_add_liquidity` — VIZ уходит в кривую рынка, вы получаете долю пула пропорционально вкладу. Цена от вашего внесения не смещается (вы добавляете симметрично). Минимальный взнос — `pm_min_liquidity` (управляемый параметр, по умолчанию 100 VIZ): **тот же пол, что и при создании рынка**, он действует и на пополнение уже открытого рынка. Причина — техническая: каждый вызов создаёт ОТДЕЛЬНУЮ строку ликвидности (взносы не сливаются в одну позицию), а сеттлмент обходит все такие строки, поэтому дешёвых микровзносов быть не должно. Хотите добавить меньше — докладывайте реже и крупнее. + +**Пока рынок открыт.** Со ставок удерживается комиссия и распределяется LP по долям. Поздние ставки платят anti-sniping-штраф, который тоже идёт в пользу ликвидности. Ваш доход капает по мере оборота. + +**Выводите (по желанию).** `pm_withdraw_liquidity`, частично или полностью. Вывод price-neutral: вернётся ваш **принципал** (комиссия реализуется только на сеттле — см. ниже), кривая не сдвинется. Никакого impermanent loss, как в классических AMM: round-trip вход-выход не двигает цену и не съедает капитал. + +**Сеттлмент.** Когда рынок разрешён, ликвидность возвращается **сама**, по мере per-block-сеттла. `settle_liquidity` отдаёт каждому LP принципал безусловно + бонус. Ликвидность заперта с закрытия ставок до сеттлмента (пока считаются выплаты), потом освобождается. + +## Что нужно понимать активному LP + +- **Principal-protected — это про исход, а не про всё на свете.** Вы не теряете принципал от того, кто выиграл рынок. Ваш доход — комиссии и штрафы; риск исхода на вас не переложен. +- **Вы не контрагент плеча.** Заём плечевых трейдеров фронтит **ленивый пул** (пассивный продукт), а не ваша прямая ликвидность. Плечо вашего принципала не касается. (Есть тонкий дизайн-нюанс с покрытием сверхприбыли плеча — он локализован на пул, не на прямых LP.) +- **Глубина = объём = доход.** Тонкая ликвидность отпугивает крупных беттеров; ваш вклад напрямую влияет на качество рынка и, через объём, на ваши комиссии. +- **Лок на время сеттла — нормально.** С закрытия ставок до расчёта выплат ликвидность заперта; это не потеря, а порядок расчёта. Дальше принципал возвращается. +- **Доход реализуется на сеттле.** Заработанная комиссия (earned_fee) окончательно оформляется при разрешении рынка; до резолва она отражается, но фиксируется в возврате. +- **Ранний вывод = только принципал.** `pm_withdraw_liquidity` требует активную строку (`status == 0`), а счётчик комиссии присваивается вместе с `status = 3` на сеттле — поэтому ранний вывод не может увидеть никакой комиссии; вы просто получаете принципал обратно, price-neutral. Отдельного «дохода на текущий момент» забрать нельзя. +- **VOID-рынки дохода не платят.** Если рынок закрылся void (no contest — оракул не дал победителя, например источник отменился или 50/50), ставки рефандятся, а LP получают принципал с **нулевым** бонусом. Доход LP бывает только на рынках, разрешившихся победителем. + +## Роли рядом с вами + +- **Создатель рынка** — вносит стартовую ликвидность (он тоже прямой LP) и задаёт комиссию. +- **Беттер** — платит комиссию, из которой складывается ваш доход. +- **Пассивный LP (ленивый пул)** — другой продукт: пассивный капитал, который фронтит плечо; не путать с прямой ликвидностью рынка. + +Дальше по теме: «Пассивный LP (ленивый пул)» (в чём разница и где риск плеча), «Почему пул, а не коэффициенты» (как устроена кривая), «Создатель рынка» (как задаётся комиссия и глубина). diff --git a/@l10n/ru/docs/prediction-markets/guides/bettor.md b/@l10n/ru/docs/prediction-markets/guides/bettor.md new file mode 100644 index 0000000000..db0f4c0b5a --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/bettor.md @@ -0,0 +1,42 @@ +--- +title: "Беттер — как делать ставки на рынках предсказаний VIZ" +description: "Ставка идёт в общий пул рынка и двигает плавающий коэффициент; выигрыш делится между угадавшими пропорционально долям, выплата автоматическая на резолве." +--- + +# Беттер: как делать ставки на рынках предсказаний VIZ + +Вы пришли поставить на исход — «да/нет», «кто победит», «будет ли событие». На VIZ это работает не как у букмекера с фиксированным коэффициентом, а как **общий пул**. Разберём, что именно происходит с вашими деньгами и как формируется выигрыш. + +## Главное в двух абзацах + +Вы ставите VIZ на один из исходов. Ваша ставка идёт в **пул рынка** и двигает цену: чем больше поставили на исход, тем дороже он становится и тем меньше «долей» (weight) вы получаете за следующий токен. Коэффициент не фиксируется в момент ставки — он **плавающий**, его задаёт баланс пула прямо сейчас. + +Когда оракул объявляет исход, весь пул делится между теми, кто угадал, пропорционально их долям. Проиграл — теряешь ставку, она уходит победителям. Выиграл — забираешь свою долю призового пула. Никаких «зафиксировал 2.5 и жди» — коэффициент вы видите как текущую цену, и он меняется по мере того, как другие ставят. + +## Что происходит по шагам + +**Ставка.** Вы выбираете исход и сумму. Нода считает по кривой пула, сколько долей (weight) вам причитается за эту сумму по текущей цене, и записывает позицию. Сумма уходит в пул, цена исхода сдвигается вверх. + +**Пока рынок открыт.** Цена живёт: другие ставят, коэффициент плывёт. Вы можете поставить ещё, а на рынках с разрешённой отменой — **отменить** ставку до закрытия (`pm_cancel_bet`). Важно: отмена продаёт вашу позицию обратно по текущей кривой, а не возвращает номинал. Если после вашей ставки рынок сместился, вернётся меньше внесённого — это не штраф, а честная цена выхода; на руки — никогда не больше вашей ставки (а если цена ушла в вашу пользу, излишек по кривой становится отложенным требованием к исходу, а не отдаётся рынку). Детали — «[Отмена ставки](./cancel-bet)». + +**Закрытие ставок.** В момент `betting_expiration` приём ставок прекращается. Дальше — ожидание исхода от оракула. + +**Разрешение.** Оракул объявляет победивший исход (`pm_resolve_market`). Выплата победителям — **автоматическая** и виртуальная: не нужно нажимать «забрать», выигрыш начисляется на баланс на резолве. Проигравшие исходы обнуляются, их деньги — в призовом поте победителей. + +**Если исхода нет.** Событие отменено или источник пропал — оракул объявляет **no-contest** (`pm_no_contest`): ставки возвращаются, никто не выигрывает и не проигрывает. А если оракул вовсе промолчал до дедлайна, рынок воидится как **missed-resolution** — ставки вам всё равно возвращаются (оракула при этом штрафуют). В любом случае ваши деньги не «сгорают». + +## Что нужно понимать беттеру + +- **Коэффициент — не фикс.** Цена, которую вы видите, — это состояние пула сейчас. Ранняя ставка на непопулярный исход даёт больше долей (дешевле вошли); когда толпа набежит, цена уже другая. Подробно — в статье «Почему пул, а не коэффициенты». +- **Выплата пропорциональна долям, а не «ставке × коэффициент».** Вы делите призовой пот с другими победителями по весу позиций. Итоговый множитель зависит от того, как в итоге распределились ставки по исходам. +- **Забирать вручную не надо.** Выигрыш и возвраты приходят автоматически на резолве/отмене. +- **Оракулу можно возразить.** Если исход объявлен неверно, в окне спора его можно оспорить (`pm_dispute_create`) — см. статью про диспуты. +- **Ликвидный баланс.** Для ставки нужен свободный VIZ; застейканные SHARES не считаются. Кошелёк подскажет, если не хватает. + +## Роли рядом с вами + +- **Оракул** — тот, кто объявит исход и отвечает за это репутацией и залогом-страховкой. +- **Поставщик ликвидности** — тот, чей капитал задаёт глубину кривой (чтобы ваша ставка не двигала цену слишком резко). +- **Плечевой трейдер** — ставит на **цену** с заёмом у пула, а не на исход; отдельный инструмент. + +Дальше — по вкусу: «Почему пул, а не коэффициенты» (механика цены), «Оракул и разрешение» (кто и как объявляет исход), «Диспуты» (как оспорить). diff --git a/@l10n/ru/docs/prediction-markets/guides/cancel-bet.md b/@l10n/ru/docs/prediction-markets/guides/cancel-bet.md new file mode 100644 index 0000000000..641c7942dc --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/cancel-bet.md @@ -0,0 +1,37 @@ +--- +title: "Отмена ставки — выход по цене кривой" +description: "pm_cancel_bet продаёт позицию обратно по текущей кривой (curve-priced), а не возвращает номинал. На руки возврат никогда не больше вашей ставки (cap-at-stake): убыточная разница уходит в forfeit_pool рынка, прибыльный избыток — в отложенное требование к исходу. Отмена — до закрытия ставок, если рынок её разрешает." +--- + +# Отмена ставки: выход по цене кривой + +Передумали до закрытия рынка? На рынках, где это разрешено, ставку можно отменить (`pm_cancel_bet`). Но важно понимать: это **не** «верните мои деньги», а продажа позиции обратно по текущей цене. Разберём, сколько вернётся и почему. + +## Главное в двух абзацах + +Отмена — это выход по **текущей кривой** (curve-priced), а не возврат номинала. Вы продаёте свои доли (weight) по цене, которая сложилась к моменту отмены. Если после вашей ставки рынок сместился, вернётся **меньше** внесённого — это честная цена выхода, а не штраф. И наоборот: на руки возврат **никогда не больше вашей ставки** (cap-at-stake) — прибыль по кривой нельзя обналичить в момент отмены. + +Что происходит с разницей — зависит от направления. Вышло по кривой **меньше** ставки (цена против вас) → недостача (`curve_residual`) **остаётся рынку**, уходит в `forfeit_pool` и распределяется победителям на сеттле. Вышло **больше** ставки (цена в вашу пользу) → избыток не отдаётся рынку и не платится сразу: он становится вашим **отложенным требованием, привязанным к исходу**, и гасится на сеттле, если ваш исход выиграл (детали — «[Ранний выход и отложенное требование](./early-exit)»). Токены при этом не минтятся и не сжигаются. Отменить можно только до закрытия ставок (`betting_expiration`) и только если рынок создан с разрешённой отменой. + +## Как это работает + +**Продажа по кривой.** Нода считает, сколько стоят ваши доли по текущим резервам пула, и возвращает эту сумму. Цена — та же кривая, что двигали все ставки; ваш выход её тоже слегка сдвигает обратно. + +**Cap-at-stake.** Немедленный возврат ограничен сверху вашей исходной ставкой. Даже если цена вашего исхода выросла и «по кривой» доли стоят больше — на руки вернётся не больше, чем вы внесли. Избыток по кривой не пропадает: он переносится в **отложенное требование** и гасится на резолве, если ваш исход выиграл. Отмена — это выход, а не способ снять прибыль мгновенно. + +**Residual → forfeit_pool.** Если по кривой вышло меньше номинала, разница (`curve_residual`) не теряется в никуда: она направляется в `forfeit_pool` рынка и достаётся победителям на сеттле. Токены сохраняются — это часть конструкции, которая держит консервацию (никакого «сжигания» и никакого молчаливого минта). + +## Что нужно понимать + +- **Возврат ≠ номинал.** Сместился рынок после вашей ставки — вернётся меньше. Это цена ликвидности, а не наказание. +- **Мгновенно на отмене не заработать.** Cap-at-stake: на руки максимум — своя ставка. Прибыль по кривой становится отложенным требованием и приходит на резолве, если ваш исход выиграл. +- **Убыточная разница остаётся рынку.** Вышло меньше ставки — недостача уходит победителям через forfeit_pool, а не «в воздух». Прибыльный избыток, наоборот, закрепляется за вами как отложенное требование. +- **Только до закрытия и только где разрешено.** После `betting_expiration` отмены нет; на рынках без флага отмены — тоже. +- **Плечо закрывается иначе.** Выход из плечевой позиции (`pm_leverage_close`) — своя механика (заём, cancel_value); там residual тоже роутится в forfeit_pool. См. [Плечевой трейдер](./leverage-trader). + +## Связки + +- [Беттер](./bettor) — жизненный цикл ставки, где отмена — один из путей. +- [Ранний выход и отложенное требование](./early-exit) — куда идёт прибыльный хвост отмены. +- [Почему пул, а не коэффициенты](./why-pool-not-odds) — почему цена (и цена выхода) плавает. +- [Спецификация](../specification) — формула curve-priced cancel и маршрут residual. diff --git a/@l10n/ru/docs/prediction-markets/guides/commit-reveal.md b/@l10n/ru/docs/prediction-markets/guides/commit-reveal.md new file mode 100644 index 0000000000..41c5046b13 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/commit-reveal.md @@ -0,0 +1,44 @@ +--- +title: "Скрытые ставки — commit-reveal и батчи" +description: "Скрытая ставка: сначала commit (эскроу без раскрытия исхода/суммы), затем reveal в окне. Приватность до раскрытия защищает от копирования и снайпинга. Не раскрыл — форфейт части эскроу в forfeit_pool. Нужен рынок с allow_batch." +--- + +# Скрытые ставки: commit-reveal и батчи + +Иногда важно, чтобы вашу ставку **не видели заранее** — чтобы её не скопировали и не сыграли против неё. Для этого есть двухфазная скрытая ставка: сначала вы «запечатываете» её (commit), потом раскрываете (reveal). Разберём, зачем это и что будет, если не раскрыть. + +## Главное в двух абзацах + +Обычная ставка видна в цепи сразу — исход, сумма, время. Скрытая ставка прячет это до раскрытия: вы вносите **эскроу** и коммитите ставку (`pm_commit_bet`), не показывая, на какой исход и сколько. Позже, в **окне раскрытия**, вы её раскрываете (`pm_reveal_bet`), и она встаёт в пул как обычная. До раскрытия никто не знает вашу позицию — это защита от копирования, фронт-раннинга и снайпинга крупной ставки. + +За приватность есть ответственность: если вы **не раскрыли** ставку в окне, срабатывает **форфейт** — часть эскроу (штраф) уходит в `forfeit_pool` рынка (победителям), остальное возвращается. Скрытые ставки работают только на рынках, где включён батч-режим (`allow_batch`), и требуют минимального эскроу (`pm_min_batch_bet`). + +## Как это работает по шагам + +**Commit.** `pm_commit_bet`: вносите эскроу (≥ `pm_min_batch_bet`, порядка 1 VIZ), коммитите ставку в закрытом виде. В цепи видно, что вы что-то поставили, но не видно исход/сумму. Эскроу списан. + +**Окно раскрытия.** Даётся ограниченное окно (эпоха + `pm_reveal_window_blocks`, порядка минут). В нём вы обязаны раскрыть. + +**Reveal.** `pm_reveal_bet`: показываете исход и сумму, ставка встаёт в пул по текущей цене — дальше как обычная ставка. Чтобы раскрыть, клиенту нужен `commit_id` вашего коммита (`get_account_commits`). + +**Не раскрыл → форфейт.** Пропустили окно — автоматический форфейт: штраф (доля эскроу, задаётся `no_reveal_fee_percent`) → `forfeit_pool` рынка, остаток возвращается. Это цена за то, что вы заняли слот и не завершили ставку. + +## Зачем это нужно + +- **Приватность намерения.** Крупный игрок не хочет, чтобы его ставку скопировали или сыграли против неё до того, как она встанет в пул. +- **Анти-снайпинг.** Скрытая фаза мешает подсматривать и опережать чужие ставки. +- **Батчи.** Коммиты собираются и раскрываются пачками — режим для более честного и приватного набора ставок. + +## Что нужно понимать + +- **Две фазы, два действия.** Commit и reveal — разные операции; между ними окно. Забыли раскрыть — потеряли штраф. +- **Раскрытие обязательно.** Форфейт — не баг, а стимул завершать начатое; штраф идёт победителям через forfeit_pool. +- **Только на allow_batch-рынках.** Не всякий рынок поддерживает скрытые ставки. +- **Нужен commit_id.** Для reveal клиент тянет ваши открытые коммиты (`get_account_commits`) — без него раскрыть нечего. +- **Kill-switch.** Весь commit-reveal — подсистема с медиан-переключателем (`pm_commit_reveal_enabled`); валидаторы могут её отключить без хардфорка. + +## Связки + +- [Беттер](./bettor) — обычная (видимая) ставка, для сравнения. +- [Почему пул, а не коэффициенты](./why-pool-not-odds) — куда встаёт раскрытая ставка. +- [Спецификация](../specification) — окна раскрытия, `no_reveal_fee_percent`, батч-механика. diff --git a/@l10n/ru/docs/prediction-markets/guides/coupon.md b/@l10n/ru/docs/prediction-markets/guides/coupon.md new file mode 100644 index 0000000000..c0d4e01565 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/coupon.md @@ -0,0 +1,40 @@ +--- +title: "Купон — несколько ставок одной транзакцией" +description: "Купон собирает N ставок (pm_place_bet) в одну подписанную транзакцию VIZ. Транзакция атомарна: любая негодная нога отклоняет весь купон, половины не бывает. Это не экспресс — ноги независимы, выплаты не перемножаются." +--- + +# Купон: несколько ставок одной транзакцией + +Купон — это способ поставить сразу на несколько исходов, не подписывая каждую ставку по отдельности. Вы набираете линии тапами по исходам, задаёте суммы и отправляете всё **одной транзакцией**. + +## Главное в двух абзацах + +Технически купон — это N операций `pm_place_bet` в одной подписанной транзакции VIZ. Транзакция в цепи **атомарна на записи**: она либо применяется целиком, либо отклоняется целиком. Значит, если хотя бы одна нога негодная — рынок успел закрыться, не хватило баланса на сумму всех ног, сработала защита от проскальзывания — **не пройдёт весь купон**. Состояния «половина ставок поставилась» не существует, и разбирать частичный результат не придётся. + +При этом купон **не экспресс (не парлей)**. Каждая нога — обычная самостоятельная ставка в пул своего рынка, со своей выплатой. Коэффициенты не перемножаются: выигрыш по одной ноге не зависит от того, зашли остальные или нет. Купон экономит подписи и делает набор ставок одномоментным — но не создаёт связку «все исходы должны сойтись». Настоящая связка с перемножением — отдельный протокольный примитив, и на уровне ставок его сейчас нет. + +## Как это работает + +**Сбор.** В ленте событий и на странице события (вид «Линии») тап по исходу кладёт ногу в купон: рынок, исход, сумма (по умолчанию 1 Ƶ) и ваша защита от проскальзывания. Повторный тап по тому же исходу обновляет ногу, а не плодит дубли. Купон живёт локально в браузере — до отправки он никак не касается цепи. + +**Проверка перед отправкой.** Купон, собранный вчера, может тащить рынок, ставки на котором уже закрыты. Такую ногу цепь отвергнет — а вместе с ней и всю транзакцию, поэтому клиент проверяет каждую ногу при открытии купона тем же правилом, что и нода (рынок активен и либо бессрочный, либо дедлайн ещё не наступил), помечает мёртвые и не даёт отправить, пока их не уберут. Отдельно проверяется ликвидный баланс на **сумму всех ног**: пул принимает ставки только свободными VIZ, застейканные доли не считаются. + +**Отправка.** Все ноги подписываются одним ключом (active) и уходят одной транзакцией. Комиссии сети нет — ограничение задаёт энергия аккаунта, и по ней транзакция из N ставок дешевле, чем N отдельных. + +**Что дальше.** После записи каждая нога живёт своей жизнью: своя доля в пуле своего рынка, свой резолв, своя автоматическая выплата. В «Моей активности» они видны как обычные ставки. + +## Что нужно понимать + +- **Всё или ничего — на записи, а не на исходе.** Атомарность купона про попадание в блок, а не про угадывание. Одна нога проиграла — остальные всё равно считаются и платят. +- **Выплаты не перемножаются.** Это не экспресс. Хотите большего риска — увеличивайте сумму ноги, а не количество ног. +- **Одна мёртвая нога рушит отправку.** Закрывшийся рынок, недостаток свободных VIZ на сумму всех ног, сработавшая защита минимума долей — и цепь отклонит купон целиком. Клиент подсвечивает такие ноги заранее. +- **Скрытые ставки в купон не идут.** Commit-reveal и батч-режим — отдельные пути со своим окном раскрытия; в купон собираются только обычные («мгновенные») ставки. +- **Купон хранится в браузере.** Пока вы его не отправили, это черновик на вашем устройстве: цепь о нём не знает, и на другом устройстве его не будет. + +## Связки + +- [Беттер](./bettor) — жизненный цикл одиночной ставки, из которых состоит купон. +- [Почему пул, а не коэффициенты](./why-pool-not-odds) — почему цена ноги плавает и зачем защита минимума долей. +- [События и метаданные](./events-metadata) — как линии одного матча собираются в карточку события, из которой удобно набирать купон. +- [Скрытые ставки (commit-reveal)](./commit-reveal) — путь, который в купон не входит, и почему. +- [Операции `pm_*`](../../protocol/operations/prediction-markets) — `pm_place_bet`, из которых собирается транзакция. diff --git a/@l10n/ru/docs/prediction-markets/guides/disputes.md b/@l10n/ru/docs/prediction-markets/guides/disputes.md new file mode 100644 index 0000000000..fae56ed5c9 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/disputes.md @@ -0,0 +1,44 @@ +--- +title: "Диспуты — как оспорить исход" +description: "Если оракул объявил неверный исход, его можно оспорить в grace-окне (pm_dispute_create, залог-fee). Голосование разрешает спор (committee или account-режим); неверный резолв штрафует оракула, диспутер рискует залогом. Выплаты могут пересчитаться." +--- + +# Диспуты: как оспорить исход + +Оракул — человек или сервис, он может ошибиться или схитрить. Диспут — это предохранитель: механизм, которым участники оспаривают объявленный исход, пока выплаты ещё можно пересчитать. Разберём, как это работает и чем рискует каждая сторона. + +## Главное в двух абзацах + +После того как оракул объявил исход (`pm_resolve_market`), открывается **окно спора** (grace-период, `pm_dispute_grace_sec`, порядка 12 часов). В этом окне любой участник может подать диспут (`pm_dispute_create`), внеся **залог-fee** (около 1000 VIZ) — это плата за то, чтобы сеть пересмотрела результат, и защита от спама пустыми оспариваниями. + +Спор разрешается **голосованием** (`pm_dispute_vote`, active-ключом) — по одному из режимов, заданных создателем рынка: комитетный (доверенные арбитры) или account-голосование. Если диспут признаёт резолв неверным — исход исправляется, выплаты пересчитываются, **оракула штрафуют** (залог/репутация), а диспутер получает награду. Если резолв признан верным — диспутер теряет залог. Ставки на кону с обеих сторон, поэтому оспаривают по делу, а не наугад. + +## Как это работает по шагам + +**Резолв и старт окна.** Оракул объявил исход → выплаты начислены → пошёл grace-таймер спора. Пока он идёт, результат не окончателен. + +**Подача диспута.** `pm_dispute_create` с залогом-fee. Рынок переходит в статус спора (payout откладывается/помечается), появляется запись диспута с указанием оракула и инициатора. Эмитится vop `pm_dispute_opened` (попадает в историю и оракула, и диспутера). + +**Голосование.** Участники режима голосуют `pm_dispute_vote` (active-ключом). Режим и состав определены при создании рынка (комитет или аккаунты). Голоса собираются до дедлайна ответа/решения. + +**Финализация.** Спор закрывается (`pm_dispute_finalize`) по итогам голосования, либо авто-закрывается (`pm_dispute_auto_close`), если оракул не ответил в срок. Итог: +- **Резолв неверен** → исход меняется, выплаты пересчитываются под правильный исход, оракул штрафуется, диспутер вознаграждается. +- **Резолв верен** → результат остаётся, диспутер теряет залог, репутация оракула подтверждается. + +**Влияние на выплаты.** Пока идёт спор, финальные выплаты по оспоренному рынку не считаются окончательными. После финализации сеттлмент проходит по подтверждённому исходу. + +## Что нужно понимать + +- **Окно ограничено.** Оспорить можно только в grace-периоде после объявления. Опоздал — исход окончателен. При досрочном резолве (`allow_early_resolution`) окно схлопывается к моменту резолва, но полный `pm_dispute_grace_sec` от анонса диспутерам сохраняется. +- **Залог — с обеих сторон.** Диспутер рискует fee, оракул — залогом и репутацией. Это делает спор дорогим для обмана и дешёвым для правды. +- **Режим задаёт создатель.** Комитетный режим — быстрее и предсказуемее (доверенные арбитры); account-режим — децентрализованнее. Смотрите режим рынка перед крупной ставкой. +- **Голос — active-ключом.** Как и большинство PM-операций участника (`pm_dispute_vote`, `pm_dispute_create`). +- **Диспут — крайняя мера.** Для честного оракула с хорошим `reliability_score` споры редки; систематические проигранные споры роняют его репутацию и insurance. + +## Роли рядом + +- [Оракул](./oracle) — тот, чей резолв оспаривается; несёт залог и репутацию. +- [Беттер](./bettor) — может инициировать спор, если исход объявлен неверно. +- [Создатель рынка](./market-creator) — задаёт режим и окно спора при создании. + +Дальше по теме: [Оракул](./oracle) (за что штрафуют и как считается репутация), [Беттер](./bettor) (жизненный цикл ставки), [Спецификация](../specification) (формальные правила диспутов и дедлайны). diff --git a/@l10n/ru/docs/prediction-markets/guides/early-exit.md b/@l10n/ru/docs/prediction-markets/guides/early-exit.md new file mode 100644 index 0000000000..22e7b2034b --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/early-exit.md @@ -0,0 +1,43 @@ +--- +title: "Ранний выход и отложенное требование" +description: "Прибыль от раннего выхода (отмена ставки или закрытие плеча по кривой) не выплачивается сразу: она записывается как отложенное outcome-contingent требование и гасится на сеттле из ограниченной доли проигравшего пула. Принципал возвращается сразу, LP защищён." +--- + +# Ранний выход и отложенное требование + +Рынок VIZ — гибрид: вход и ранний выход идут по **кривой** (как на AMM), а расчёт удержанных до конца позиций — **parimutuel** (общий пул). Из-за этого «прибыль от раннего выхода» устроена хитрее, чем кажется. Разберём, почему выигрыш от досрочного выхода приходит не сразу. + +## Главное в двух абзацах + +Когда вы выходите досрочно — отменяете ставку (`pm_cancel_bet`) или закрываете плечо (`pm_leverage_close`) — цена вашей позиции по кривой может оказаться **выше** вашей ставки. Но выплатить эту прибыль прямо сейчас нельзя: она не обеспечена проигравшими (рынок ещё не разрешён), и если её отдать из кривой — недостача ляжет на поставщиков ликвидности, а LP на VIZ **principal-protected**. Поэтому прибыльный «хвост» раннего выхода не кэшируется на месте. + +Вместо этого система возвращает вам **принципал сразу и безусловно** (свои деньги, не больше ставки), а прибыльный хвост записывает как **отложенное требование, привязанное к исходу** (outcome-contingent deferred claim). Оно гасится **на сеттле** из **ограниченной доли проигравшего пула** — то есть только если ваш выбранный исход выиграл и в поте есть чем платить. Так консервация токенов держится, а LP не субсидируют трейдеров. + +## Как это работает + +**Принципал — сразу.** Отмена/закрытие возвращает `min(цена_по_кривой, ваша_ставка)` немедленно. Ранний выход может срезать убыток или выйти в ноль, но **не** реализует прибыль в момент выхода. + +**Прибыльный хвост — в отложенное требование.** Разница `max(цена_по_кривой − ставка, 0)` записывается как требование на **выбранный вами исход**. Не кэш, а «если этот исход победит — доплатим на сеттле». + +**Гасится из ограниченного пула.** На разрешении требование оплачивается из **bounded-доли проигравшего пула** (не из кривой, не из LP-принципала). Нет проигравших денег под это — выплата урезается. Никакого молчаливого минта. + +**Честная цена входа (защита от накрутки).** Сплит «принципал/хвост» пересчитывается по глубине кривой **на момент вашей ставки**, а не текущей. Это закрывает трюк «поставил → сам добавил ликвидности → раздул глубину → снял больший хвост»: нормализация может только уменьшить выплату, не увеличить. + +## Почему так + +- **LP обещан principal-protected.** Если бы прибыль раннего выхода платилась из кривой, недостача (когда прибыль обгоняет проигравшие ставки) списывалась бы с принципала LP или минтилась — оба варианта ломают гарантию. Отложенное требование переносит выплату на сеттл, где источник — проигравшие, а не LP. +- **Parimutuel обеспечен только проигравшими.** Прибыль победителя = чужой проигрыш. До резолва «проигравших» ещё нет — значит и прибыль не из чего платить сразу. + +## Что нужно понимать + +- **Досрочно вы забираете принципал, не прибыль.** Прибыльный хвост ждёт резолва. +- **Хвост условен по исходу.** Выбранный исход проиграл — хвоста нет; выиграл — гасится из пота (в пределах доступного). +- **Это защищает LP и консервацию.** Механизм намеренный, а не ограничение кошелька. +- **Плечо — тот же принцип.** Прибыль плеча тоже приходит после разрешения, из ограниченного пула. См. [Плечевой трейдер](./leverage-trader). + +## Связки + +- [Отмена ставки](./cancel-bet) — как считается возврат при досрочном выходе. +- [Плечевой трейдер](./leverage-trader) — почему прибыль плеча отложена. +- [Активный LP](./active-lp) — чью защиту это обеспечивает. +- [Спецификация](../specification) — формальная модель deferred-claim и bounded-slice. diff --git a/@l10n/ru/docs/prediction-markets/guides/events-metadata.md b/@l10n/ru/docs/prediction-markets/guides/events-metadata.md new file mode 100644 index 0000000000..47b330225f --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/events-metadata.md @@ -0,0 +1,107 @@ +--- +title: "События и метаданные — как клиент собирает рынки в матчи" +description: "Как Forecaster агрегирует рынки в «События»: ключи метаданных event / event_title / child, родительский и дочерние рынки, что индексирует нода и как создать рынок, который клиент распознает как событие." +--- + +# События и метаданные: как рынки собираются в матчи + +Один реальный матч — это обычно несколько рынков: «кто победит», «тотал убийств», «первый Рошан». +On-chain это независимые `pm_market_object`, но клиент (Forecaster) показывает их одной карточкой +события со ставками по исходам и ссылкой «ещё N линий». Склейка происходит **только через +метаданные** — специальных «родительских» объектов в консенсусе нет, и это осознанно: протокол +остаётся минимальным, а группировку задаёт создатель рынка при создании. + +## Короткая модель + +- Каждый рынок несёт свободное текстовое поле `metadata` (JSON-строка в `pm_create_market`). +- Нода парсит из него **белый список ключей** и строит индексы; всё остальное игнорирует. +- Рынки с одинаковым `event` — «одно событие». Рынок без `child` — лицевой (родительский), + с `child: 1` — дочерняя линия (проп). +- Forecaster: таб «События» группирует активные рынки по `event`, лицевым показывает + винлайн-рынок, дочерние прячет из общих лент и раскрывает на странице события. + +## Ключи метаданных, которые индексирует нода + +Нода извлекает из `metadata` только эти поля (остальные ключи не индексируются, но остаются +в сыром JSON — клиенты могут читать их сами): + +| Ключ | Тип | Зачем | +|------|-----|-------| +| `title` | строка | Человекочитаемый вопрос рынка (заголовок карточки). | +| `category` | строка | Раздел листингов (`esports`, `sports`, `crypto`…) — индекс `by_category`. | +| `subcategory` | строка | Уточнение раздела (опционально). | +| `tags` | массив или CSV | Теги для фильтров; клиент читает **массив** `market.metadata.tags`, который нода пересобирает сама. | +| `image` | строка (URL) | Обложка карточки (ссылка, не хостится on-chain). | +| `description` | строка | Краткие правила резолва — «как оракул решит исход». | +| `event` | строка (slug) | **Ключ склейки события.** Все рынки одного матча ставят одинаковый `event`. | +| `event_title` | строка | Человекочитаемое имя события («Dota 2: MOUZ vs Vici — TI 2026»). | +| `child` | 1 / true | **Дочерняя линия (проп).** Прячется из лент по категории/тегу; видна на странице события. | +| `banned_jurisdictions` | массив или CSV | Юрисдикционный фильтр клиентов. | +| `condition_id` | строка | Дедуп-идентификатор источника (для зеркалирующих парсеров). | + +Правила разбора: `metadata` должен быть валидным JSON-объектом (не-JSON просто не индексируется); +`tags`/`banned_jurisdictions` принимаются и массивом, и CSV-строкой; `child` принимается как +`true`, `1` или `"1"`. Теги матчатся без учёта регистра. + +## Как указать «родительский» рынок + +Родитель не указывается явно — он **вычисляется отсутствием `child`**: + +1. Всем рынкам матча проставьте одинаковый `event` (стабильный slug: латиница, дефисы — + например `dota2-mouz-vg-2026-07-12`) и одинаковый `event_title`. +2. Главному рынку матча («кто победит» / moneyline) — **не ставьте** `child`. Это и есть + родитель: он остаётся видимым во всех лентах и станет лицом карточки события. +3. Всем остальным линиям (тоталы, форы, спецрынки) — `child: 1`. Они исчезают из общих + лент (не шумят в категориях), но полностью доступны на странице события и по прямой ссылке. + +Минимальный пример `metadata` для трёх рынков одного матча: + +```json +// Родитель (винлайн) — БЕЗ child +{"title":"MOUZ победит Vici Gaming?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming"} + +// Дочерняя линия 1 +{"title":"Тотал убийств больше 45.5 (карта 1)?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming","child":1} + +// Дочерняя линия 2 +{"title":"Первый Рошан — MOUZ?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming","child":1} +``` + +Важно: `event` **иммутабелен на практике** — клиенты склеивают по точному совпадению строки, +поэтому выберите ключ до создания рынков и используйте его во всех линиях матча одинаково +(регистр и дефисы имеют значение). + +## Что делает нода + +- Строит мета-объект рынка (`pm_market_meta`) с распарсенными полями и индексами: по категории, + тегам и **по событию** (`by_meta_event`); мета едет в снапшот. +- `list_markets_by_category(...)` по умолчанию **прячет дочерние** (`hide_children = true`, + 8-й аргумент) — ленты показывают только родителей; передайте `false`, чтобы увидеть все линии. +- `list_markets_by_event(event, from, limit)` отдаёт **все** рынки события — родителя и детей, + без фильтра. Это API страницы события. +- В строках листингов `event_title` отдаётся на верхнем уровне, в полной карточке рынка — внутри + `metadata`; `tags` нода пересобирает в массив. + +## Что делает Forecaster с этим + +- **Таб «События»** (спорт-бук вид): активные рынки группируются по `event`; лицевым выбирается + рынок, чей заголовок похож на винлайн (`winner` / `moneyline` / `to win`), иначе — первый + бинарный; на карточке — исходы с текущими коэффициентами (тап кладёт ногу в купон) и ссылка + «ещё N линий». +- **Страница события** `#/event/<ключ>` — все линии матча одним списком (`list_markets_by_event`). +- **Карточки** показывают `event_title` над вопросом; крошки «Категория › теги» ведут в листинги. +- **Ленты по категории/тегу** не показывают дочерние рынки — пропы живут за карточкой события. + +Практический вывод для создателя: правильно проставленные `event`/`event_title`/`child` — это +разница между «десять разрозненных рынков шумят в ленте» и «одна аккуратная карточка матча со +всеми линиями внутри». Ошиблись в ключе события — линии не склеятся; забыли `child` — пропы +засорят общие ленты. + +## Смотрите также + +- [Создатель рынка](./market-creator) — стартовая ликвидность, оракул, комиссия. +- [Мульти-исходные рынки](./multi-outcome) — когда вместо нескольких бинарных лучше один LMSR. +- [Спецификация](../specification) — формальная модель объектов и индексов. diff --git a/@l10n/ru/docs/prediction-markets/guides/index.md b/@l10n/ru/docs/prediction-markets/guides/index.md new file mode 100644 index 0000000000..15b732fdc6 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/index.md @@ -0,0 +1,44 @@ +--- +title: "Гайды по рынкам предсказаний — по ролям и фичам" +description: "Пояснительные статьи для участников рынков предсказаний VIZ (Onix): каждая роль (беттер, создатель рынка, оракул, LP, плечевой трейдер) и каждая механика простым языком, со ссылками на формальную спецификацию." +--- + +# Гайды по рынкам предсказаний (Onix) + +Пояснительные статьи для **участников** — не спецификация, а «как это работает для вас». Каждая роль +объяснена от лица участника: короткая модель → по шагам → что важно понимать → связки на соседние +статьи. Формальные механики и параметры — в [Спецификации](../specification), операции — в +[Операциях](../../protocol/operations/prediction-markets). + +## По ролям — «вы участник» + +| Роль | О чём | +|------|-------| +| [Беттер](./bettor) | Делаю ставку. Плавающий коэффициент, выплата пропорционально долям, авто-выплата на резолве. | +| [Создатель рынка](./market-creator) | Завожу вопрос. Стартовая ликвидность, комиссия, выбор оракула и режима спора. | +| [Оракул](./oracle) | Объявляю исход. Insurance и risk-floor, штрафы за промах, репутация `reliability_score`. | +| [Активный LP](./active-lp) | Даю глубину кривой рынка. Доход с комиссий, principal-protected, вывод price-neutral. | +| [Пассивный LP (ленивый пул)](./passive-lp) | Кладу VIZ пассивно. Доход от плеча и комиссий, доли по equity, FIFO-вывод. | +| [Плечевой трейдер](./leverage-trader) | Ставлю на цену с заёмом у пула. Наценка + funding, ликвидация, force-close на закрытии ставок. | + +## По фичам — «как устроен механизм» + +| Фича | О чём | +|------|-------| +| [Почему пул, а не коэффициенты](./why-pool-not-odds) | Плавающая цена по кривой (CPMM/LMSR) вместо фиксированных одды. | +| [Оракул и разрешение](./resolution) | Таймлайн резолва: дедлайны, ранний резолв, missed-resolution, no-contest. | +| [Диспуты](./disputes) | Как оспорить исход: grace-окно, залог, режимы голосования, награда/штраф. | +| [Мульти-исходные рынки](./multi-outcome) | Один рынок на много исходов, LMSR vs бинарный CPMM. | +| [Отмена ставки](./cancel-bet) | Curve-priced выход, кап на stake, разница в forfeit_pool. | +| [Ленивый пул детально](./lazy-pool) | Доли по equity-цене, источники дохода, FIFO-вывод, инвариант free ≥ 0. | +| [Ранний выход и отложенное требование](./early-exit) | Прибыль досрочного выхода — deferred claim, гасится на сеттле из пота. | +| [Скрытые ставки (commit-reveal)](./commit-reveal) | Приватность до раскрытия, окно reveal, форфейт за нераскрытие. | +| [События и метаданные](./events-metadata) | Как рынки собираются в карточку матча: event / event_title / child, родитель и дочерние линии, что индексирует нода. | +| [Купон](./coupon) | Несколько ставок одной транзакцией: атомарность на записи, почему это не экспресс, что рушит отправку. | + +## Куда дальше + +- [Спецификация](../specification) — формальные правила, параметры, объектная модель. +- [Whitepaper](../whitepaper) — тезис: почему ликвидность без риска, два типа рынков, маховик. +- [Операции `pm_*`](../../protocol/operations/prediction-markets) — подписанные консенсус-операции. +- [Workflows](../workflows) — один рынок, проведённый через все роли. diff --git a/@l10n/ru/docs/prediction-markets/guides/lazy-pool.md b/@l10n/ru/docs/prediction-markets/guides/lazy-pool.md new file mode 100644 index 0000000000..e14633a67b --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/lazy-pool.md @@ -0,0 +1,58 @@ +--- +title: "Ленивый пул детально — доли, equity-цена, FIFO-вывод" +description: "Механика ленивого пула: доли минтятся по equity-цене (free+allocated−pending_withdrawals), доход от наценки плеча + funding + комиссий, FIFO-очередь вывода с инвариантом free_balance ≥ 0, штраф экстренного вывода только на награды." +--- + +# Ленивый пул детально: доли, equity-цена, вывод + +Это глубокий разбор механики ленивого пула. Если вам нужен обзор «зачем и мой ли это продукт» — начните с роли [Пассивный LP](./passive-lp); здесь — как именно считаются доли, доход и вывод. + +## Доли по equity-цене + +Когда вы депонируете (`pm_lazy_deposit`), пул минтит вам **доли** не по номиналу 1:1, а по текущей **equity-цене**: + +> equity = free_balance + allocated − pending_withdrawals + +- **free_balance** — свободный VIZ пула, не размещённый прямо сейчас. +- **allocated** — капитал, размещённый в работе (заёмы плеча, глубина). +- **pending_withdrawals** — уже обещанные, но ещё не выплаченные выводы (FIFO-очередь). + +Цена доли = equity / общее число долей. Почему так: если считать по одному `free_balance`, новый вкладчик при размещённом капитале получил бы завышенный вес в наградах (капитал в работе не виден во free). Equity-цена честно делит доход между старыми и новыми LP. Если equity ≤ 0 (краевой случай) — fallback на 1:1. + +## Откуда доход + +- **Наценка плеча (markup).** Плечевой заём фронтит пул; фиксированная наценка возвращается в yield. +- **Funding-rate.** Пока плечевая позиция открыта, с неё капает funding в пользу пула. +- **Комиссии.** Доля общих комиссий системы. + +Доход отражается в росте стоимости вашей доли — забирать/реинвестировать вручную не нужно. + +## Вывод: FIFO-очередь и инвариант free ≥ 0 + +Вывод (`pm_lazy_withdraw`, частичный по долям или полный) сжигает ваши доли сразу, но выплата зависит от свободного баланса: + +- **Хватает free_balance** → выплата мгновенная, как раньше. +- **Не хватает** (капитал в открытых плечах) → регистрируется заявка `pm_lazy_withdraw_request` в **FIFO-очередь**, и поле `pending_withdrawals` пула растёт. + +Заявки гасятся **в порядке поступления** на каждом событии возврата свободного баланса: закрытие/ликвидация плеча, конверсия, новый депозит. Жёсткий инвариант — **free_balance ≥ 0**: пул физически не платит больше, чем свободно. Это урок раннего дизайна, когда экстренный вывод мог утащить баланс в минус (пул отдавал ещё не вернувшийся капитал). + +Читать очередь: `get_lazy_withdraw_requests(account)`; состояние пула — `get_lazy_pool`; вашу позицию — `get_lazy_deposit`. + +## Экстренный вывод и штраф + +Экстренный вывод берёт **штраф только с накопленных наград**, принципал не режется. То есть вы всегда возвращаете вложенное (возможно, через очередь), а торопливость стоит части дохода, а не капитала. + +## Что нужно понимать + +- **Доли — по equity, не по номиналу.** Честное распределение дохода; при размещённом капитале это критично. +- **Вывод может ждать очередь.** Не потеря — принципал вернётся по мере разгрузки плеча; мгновенность не гарантирована. +- **free_balance ≥ 0 — закон.** Пул не уходит в минус; выплаты сверх свободного встают в FIFO. +- **Штраф — на награды, не на принципал.** Экстренный выход дешевле деньгами, чем нервами. +- **Пул — контрагент плеча.** Природа риска отличается от прямого LP рынка (см. [Активный LP](./active-lp)). + +## Связки + +- [Пассивный LP](./passive-lp) — обзорная роль (стоит ли оно вам). +- [Плечевой трейдер](./leverage-trader) — кто занимает у пула и платит наценку. +- [Активный LP](./active-lp) — прямая ликвидность рынка, для сравнения. +- [Спецификация](../specification) — формулы equity, очереди вывода, инварианты. diff --git a/@l10n/ru/docs/prediction-markets/guides/leverage-trader.md b/@l10n/ru/docs/prediction-markets/guides/leverage-trader.md new file mode 100644 index 0000000000..0fe0e79cf7 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/leverage-trader.md @@ -0,0 +1,45 @@ +--- +title: "Плечевой трейдер — ставка на цену с заёмом у пула" +description: "Плечо: ставка на движение ЦЕНЫ исхода с заёмом у ленивого пула. Flat-наценка + funding-rate, sweep-ликвидация, force-close на закрытии ставок (не ждёт оракула), нельзя открыть при <24ч до betting_expiration. Прибыль — отложенное требование до резолва." +--- + +# Плечевой трейдер: ставка на цену с заёмом у пула + +Плечо — это отдельный инструмент поверх рынка. Вы ставите не «на исход до конца», а **на движение цены** исхода, и берёте заём у ленивого пула, чтобы усилить позицию. Это ближе к маржинальной торговле, чем к обычной ставке. + +## Главное в двух абзацах + +Вы вносите залог (collateral) и открываете плечевую позицию (`pm_leverage_open`): система занимает недостающую сумму у **ленивого пула** и ставит увеличенный размер на выбранный исход. Пока цена исхода движется в вашу сторону — растёт нереализованная прибыль; против — позиция приближается к **ликвидации**. За заём вы платите пулу **flat-наценку (markup, ~10%)** и **funding-rate** со временем; это доход пассивных LP. + +Плечо **не ждёт оракула**: позиция принудительно закрывается по цене на момент **закрытия ставок** (`betting_expiration`) — плечо сеттлится по рыночной цене, а не по объявленному исходу. Если цена дошла до порога раньше — позицию ликвидируют «свипом». Прибыль от плеча оформляется как **отложенное требование** и выплачивается после разрешения рынка (из ограниченного пула проигравших), а не мгновенно. + +## Что происходит по шагам + +**Открытие.** `pm_leverage_open`: вносите залог, задаёте плечо. Пул выдаёт заём (`pool.free_balance -= loan`), суммарный размер ставится на исход. Открыть можно только если до `betting_expiration` **не меньше 24 часов** — иначе позицию негде «прожить», и открытие отклоняется. + +**Пока открыто.** Цена исхода плывёт от ставок. В вашу сторону — прибыль растёт; против — приближается ликвидация. Капает funding-rate в пользу пула. Вы платите за плечо, пока держите позицию. + +**Закрытие по своей воле.** `pm_leverage_close` — выходите по текущей цене: возвращаете заём пулу, забираете свою часть (`cancel_value`). Остаток спреда/floor (`curve_residual`) роутится в `forfeit_pool` рынка (идёт победителям на сеттле) — деньги не «замерзают». + +**Ликвидация.** Если цена дошла до порога, позицию закрывают автоматически (sweep). Залог идёт на погашение заёма пулу; что сверху — по правилам сеттла. + +**Force-close на закрытии ставок.** Не закрыли сами — на `betting_expiration` позиция закрывается принудительно по цене этого момента. Плечо **не зависит от резолва оракула**: оно про цену, а не про «кто прав в итоге». + +**Выплата прибыли.** Прибыль плеча — не мгновенный кэш: это **отложенное требование** (deferred claim), которое гасится после разрешения рынка из ограниченного пула проигравших/форфейтов (кап на выплату). Так система не минтит токены из воздуха. + +## Что нужно понимать плечевому трейдеру + +- **Вы ставите на цену, не на исход.** Плечо закрывается по цене на закрытии ставок, а не по объявлению оракула. Можно угадать «цену» и не дождаться исхода — это разные вещи. +- **Заём — у пула, и он платный.** Flat-наценка + funding-rate идут ленивому пулу. Держать позицию долго дорого; funding работает против вас со временем. +- **Ликвидация реальна.** Движение против вас закрывает позицию принудительно, залог гасит заём. Плечо усиливает и прибыль, и убыток. +- **Окно 24 часа.** Нельзя открыть плечо, если до закрытия ставок меньше суток — нужен запас, чтобы позиция могла существовать и корректно закрыться. +- **Прибыль приходит после резолва.** Не рассчитывайте на мгновенный вывод выигрыша плеча: он оформляется отложенным требованием и гасится на сеттле рынка, в пределах доступного пула. +- **Инструмент для понимающих.** Плечо сложнее обычной ставки; если нужна простая ставка на исход — см. статью «Беттер». + +## Роли рядом с вами + +- **Пассивный LP (ленивый пул)** — тот, у кого вы занимаете; ваша наценка и funding — его доход. +- **Беттер** — ставит на исход без заёма и без ликвидации; более простой путь. +- **Оракул** — разрешает рынок; на ваше плечо влияет косвенно (прибыль гасится после резолва), но закрытие плеча привязано к цене, а не к его вердикту. + +Дальше по теме: «Пассивный LP (ленивый пул)» (другая сторона вашего заёма), «Плечо (leverage) — механизм» (формулы наценки и ликвидации), «Ранний выход и отложенное требование» (как и когда приходит прибыль плеча). diff --git a/@l10n/ru/docs/prediction-markets/guides/market-creator.md b/@l10n/ru/docs/prediction-markets/guides/market-creator.md new file mode 100644 index 0000000000..03264fa768 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/market-creator.md @@ -0,0 +1,51 @@ +--- +title: "Создатель рынка — как завести вопрос на VIZ" +description: "Как создать рынок предсказаний: задать вопрос и исходы, внести стартовую ликвидность, выбрать оракула и режим спора, назначить комиссию. Ликвидность principal-protected и возвращается сама на сеттле." +--- + +# Создатель рынка: как завести вопрос + +Вы формулируете вопрос, на который люди будут ставить: «кто победит», «случится ли событие к дате», «да/нет». Создать рынок на VIZ может любой аккаунт — это обычная подписанная операция `pm_create_market`, без разрешений и модерации. Разберём, что вы задаёте и за что отвечаете. + +## Главное в двух абзацах + +Рынок — это вопрос + набор исходов + **кривая ликвидности**, по которой считается цена. Вы вносите стартовую ликвидность (свой залог), задаёте комиссию рынка, выбираете **оракула** (кто объявит исход) и **режим спора** (как исход можно оспорить). После создания рынок живёт сам: люди ставят, цена плывёт, на дедлайне оракул объявляет результат, нода автоматически рассчитывает выплаты. + +Ваш стартовый залог не «сгорает» — прямая ликвидность рынка **principal-protected**: она задаёт глубину кривой, зарабатывает на комиссиях и **возвращается вам целиком** на сеттлменте (плюс накопленная доля комиссий). Вы не контрагент ставок и не рискуете принципалом от исхода — этим рынок VIZ отличается от «банкира», который может уйти в минус. + +## Что вы задаёте при создании + +**Вопрос и исходы.** Текст вопроса, метаданные (заголовок, событие, теги, картинка-ссылка). Тип рынка: **бинарный** (два исхода, CPMM-кривая) или **мульти-исходный** (до 64 исходов, LMSR). Мультивариант удобен, когда исходов много (кто из N победит); подробно — в статье про мульти-рынки. + +**Стартовую ликвидность.** Сколько VIZ вы кладёте в кривую. Чем больше — тем «глубже» рынок: ставки двигают цену плавнее, крупные игроки входят без резких скачков. Это ваш principal-protected капитал (см. статью про активного LP). + +**Комиссию рынка.** Процент, который удерживается со ставок и идёт поставщикам ликвидности (вам и другим LP) как доход. Ограничена медиан-параметром сети сверху. + +**Оракула.** Аккаунт, который отвечает за объявление исхода. Это можете быть вы сами или доверенный специализированный оракул (например, зеркалящий Polymarket/Kalshi). Оракул несёт **залог-страховку (insurance)** и репутацию: за неверный или пропущенный резолв его штрафуют — см. статью про оракула. + +**Режим и окно спора.** Как исход можно оспорить после объявления: через комитет или голосованием аккаунтов, и в течение какого grace-периода. Это защита беттеров от неверного резолва. + +**Сроки.** `betting_expiration` — когда закрывается приём ставок; `result_expiration` — дедлайн, к которому оракул обязан объявить исход. Опция `allow_early_resolution` разрешает оракулу закрыть рынок раньше, если исход уже известен. + +## Что происходит после создания + +- **Рынок открыт.** Люди ставят, комиссии капают вам как LP. Вы можете доложить ликвидности или вывести часть (price-neutral, без потери принципала). +- **Закрытие ставок.** На `betting_expiration` приём ставок прекращается. +- **Разрешение.** Оракул объявляет исход. Нода делит пул: угадавшие получают выплаты автоматически, ваш LP-принципал + доля комиссий возвращаются на сеттле **безусловно**. +- **Если исхода нет.** Оракул объявляет **no-contest** (событие отменено, источник пропал) → ставки возвращаются беттерам, ваша ликвидность — вам, оракул без штрафа. Если же оракул **промолчал** до дедлайна (`result_expiration` + grace), рынок гибнет как **missed-resolution**: ставки тоже возвращаются, но оракула штрафуют (см. «[Оракул и разрешение](./resolution)»). + +## Что нужно понимать создателю + +- **Вы не банкир.** Прямая ликвидность рынка не покрывает выигрыши из своего кармана — призовой пул формируют проигравшие ставки. Ваш принципал защищён. +- **Оракул — критичный выбор.** От него зависит, будет ли рынок честно разрешён. Плохой оракул = споры и штрафы. Берите проверенного или будьте оракулом сами и держите insurance выше risk-floor, иначе рынок скроют из листингов. +- **Комиссия — баланс.** Выше комиссия = больше ваш доход, но дороже игрокам и меньше объём. Ниже — привлекательнее для беттеров. +- **Глубина решает.** Тонкая ликвидность = резкие скачки цены от каждой ставки, что отпугивает крупных игроков. Стартовый залог задаёт качество рынка. +- **Метаданные важны.** Рынок без on-chain-названия/тегов хуже находится в клиентах и сложнее резолвится автоматикой. + +## Роли рядом с вами + +- **Беттер** — тот, кто ставит на ваши исходы. +- **Оракул** — объявляет результат; вы его выбираете при создании. +- **Активный LP** — может добавить глубины в ваш рынок помимо вашего стартового залога. + +Дальше по теме: «Оракул и разрешение» (кого выбрать и как он отвечает), «Активный LP» (механика ликвидности и возврата принципала), «Диспуты» (режимы спора), «Мульти-исходные рынки» (когда исходов больше двух). diff --git a/@l10n/ru/docs/prediction-markets/guides/multi-outcome.md b/@l10n/ru/docs/prediction-markets/guides/multi-outcome.md new file mode 100644 index 0000000000..85700d47bb --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/multi-outcome.md @@ -0,0 +1,39 @@ +--- +title: "Мульти-исходные рынки — один вопрос, много вариантов" +description: "Мульти-рынок: один рынок на несколько исходов (кто из N победит), ценообразование по LMSR вместо CPMM бинарного. Лимит исходов — медиан-параметр pm_max_outcomes; oversize сводится к бинарным. Ставка, доли и выплата — как в пуле." +--- + +# Мульти-исходные рынки: один вопрос, много вариантов + +Не всякий вопрос сводится к «да/нет». «Кто выиграет турнир из восьми команд?», «какая партия наберёт больше?» — здесь исходов много. Мульти-рынок держит их в **одном** рынке, а не в куче отдельных бинарных. + +## Главное в двух абзацах + +Бинарный рынок (два исхода) ценится по кривой **CPMM**. Когда исходов больше — рынок использует **LMSR** (logarithmic market scoring rule): одна кривая на все исходы сразу, цена каждого зависит от того, сколько поставили на него относительно других. Сумма «вероятностей» исходов держится согласованной, и вы всегда видите относительную цену каждого варианта. + +Для вас как участника логика та же, что и в пуле: ставите на исход, получаете доли (weight) по текущей цене, на резолве призовой пот делится между угадавшими пропорционально долям. Разница — под капотом (формула цены) и в лимите: число исходов ограничено медиан-параметром сети `pm_max_outcomes`; если исходов больше лимита, рынок сводят к бинарной форме (например, «фаворит vs поле»). + +## Чем отличается от пачки бинарных + +**Один рынок вместо N.** Вместо десяти отдельных «команда X победит: да/нет» — один рынок с десятью исходами. Ликвидность не размазана по десяти пулам, цена согласована между вариантами. + +**LMSR вместо CPMM.** Бинарный CPMM держит две стороны; LMSR обобщает это на много исходов через логарифмическую функцию стоимости. Параметр «глубины» LMSR (`lmsr_b`) выводится из внесённой ликвидности и числа исходов — он задаёт, насколько дорого двигать цену. Чем больше ликвидности, тем глубже рынок. + +**Согласованные цены.** В LMSR цены исходов связаны: подорожал один — относительно подешевели остальные. Это ближе к «распределению вероятностей», чем набор независимых бинарных рынков. + +**Связь с событиями.** Крупные события (матч, турнир) часто зеркалятся из внешних источников как набор рынков под общим `event`-ключом — тогда мульти-рынок и связанные бинарные пропы группируются на странице события (см. клиент Forecaster). + +## Что нужно понимать + +- **Логика ставки не меняется.** Исход → доли по цене → доля пота на резолве. Мульти отличается ценообразованием, не сутью. +- **Лимит исходов — параметр сети.** `pm_max_outcomes` медиан-голосуется; рынок с числом исходов сверх лимита создаётся в бинарной форме, а не отклоняется молча. +- **Глубина важнее при многих исходах.** Тонкая ликвидность на мульти-рынке двигает цены резче — LMSR-глубина распределяется на все варианты. +- **Ранний вход на недооценённый вариант выгоден.** Как и в бинарном пуле: дешёвая цена = больше долей. +- **Выплата — по долям.** Никаких заранее фиксированных коэффициентов; итог складывается из финального расклада. + +## Связки + +- [Почему пул, а не коэффициенты](./why-pool-not-odds) — как вообще формируется цена в пуле. +- [Беттер](./bettor) — жизненный цикл ставки (тот же и для мульти). +- [Создатель рынка](./market-creator) — как выбрать тип рынка при создании. +- [Спецификация](../specification) — формулы LMSR, `lmsr_b`, лимиты исходов. diff --git a/@l10n/ru/docs/prediction-markets/guides/oracle.md b/@l10n/ru/docs/prediction-markets/guides/oracle.md new file mode 100644 index 0000000000..ba7763b86c --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/oracle.md @@ -0,0 +1,47 @@ +--- +title: "Оракул — как объявлять исход и отвечать за него" +description: "Оракул объявляет результат рынка (pm_resolve_market), несёт залог-страховку (insurance) и репутацию (reliability_score). Штрафы за неверный/пропущенный резолв; risk-floor скрывает недофинансированного оракула из листингов." +--- + +# Оракул: как объявлять исход и отвечать за него + +Оракул — это аккаунт, который говорит цепи, чем закончилось событие. От него зависит, кому уйдёт призовой пул. Поэтому оракул на VIZ не просто «нажимает кнопку»: он несёт **денежный залог** и **репутацию**, и отвечает ими за честность и своевременность. + +## Главное в двух абзацах + +Вы объявляете победивший исход операцией `pm_resolve_market` (только после закрытия ставок). Выплаты угадавшим начисляются автоматически. Чтобы вам доверяли рынки и деньги, вы держите **insurance** — залог в VIZ, привязанный к вашему оракул-аккаунту. Если insurance проседает ниже **risk-floor** относительно объёма обслуживаемых ставок, нода **скрывает ваши рынки из листингов** — механизм защищает беттеров от недокапитализированного оракула. + +За ошибки и молчание вас штрафуют из накопленной репутации и залога: неверный резолв, оспоренный и отменённый диспутом; пропуск дедлайна (`result_expiration`), когда рынок умирает как missed-resolution. Ваша **репутация** (`reliability_score`, 0..10000 bp) складывается из точности, вердиктов по спорам, отзывчивости и пунктуальности — и видна всем при выборе оракула. + +## Что вы делаете по шагам + +**Готовитесь.** Заводите insurance на оракул-аккаунт (`pm_oracle_update`) — держите его с запасом над risk-floor, иначе рынки скроют. Правило floor: рынок виден, если insurance ≥ порога и ≥ кратного суммы ставок под вашим управлением. + +**Ждёте закрытия ставок.** Резолвить можно только после `betting_expiration`. Раньше — нельзя (нет смысла объявлять исход, пока ставки идут). Если рынок создан с `allow_early_resolution` и исход уже определённо известен, можно закрыть досрочно — окно спора при этом схлопывается к моменту резолва. + +**Объявляете исход.** `pm_resolve_market` с победившим исходом. Нода делит пул: угадавшие получают выплаты автоматически (виртуальная `pm_payout`), проигравшие исходы обнуляются. За резолв вам начисляется **oracle fee** (ограничен медиан-параметром сети, максимум `pm_max_oracle_fee_percent`). + +**Если исхода нет.** Событие отменено, источник пропал, ничья без победителя → вместо резолва объявляете **no-contest** (`pm_no_contest`): ставки возвращаются, никто не выигрывает и не проигрывает. Это честный выход, а не штраф — но делать его надо вовремя, до дедлайна. + +**Проходите окно спора.** После объявления беттеры могут оспорить исход в grace-периоде (`pm_dispute_create`). Если диспут признаёт вашу правоту — вы получаете подтверждение репутации; если ваш резолв признан неверным — штраф. Подробно — в статье про диспуты. + +## За что штрафуют + +- **Неверный резолв.** Объявили не тот исход, диспут это подтвердил → штраф из залога/репутации, выплаты пересчитываются. +- **Пропуск дедлайна.** Не объявили исход до `result_expiration` → рынок гибнет как missed-resolution, вас слэшат. Не молчите: если источник не дал результат — делайте no-contest. +- **Систематическая медлительность.** Поздние (но сделанные) резолвы роняют фактор пунктуальности в `reliability_score`. Опоздание больше не равно своевременности. + +## Что нужно понимать оракулу + +- **Insurance — это доверие в цифрах.** Держите его выше floor с запасом; он проседает от накопленного слэша и растёт с обслуживаемым объёмом. Недофинансированный оракул исчезает из листингов — рынки будто «пропадают». +- **Репутация публична и составная.** `reliability_score` = точность (accuracy) + вердикты по спорам + отзывчивость + пунктуальность − штрафы (decaying penalty-stamps) − баны, со сжатием к средней при малом числе резолвов. В интерфейсе показывается как процент (bp/100). +- **Резолв необратим по эффекту, но оспорим.** Выплаты идут сразу, но окно спора может их отменить и наказать вас. Ошиблись — честнее самому инициировать корректировку, чем ждать слэша. +- **Своевременность = деньги.** No-contest вовремя лучше, чем молчание до дедлайна. Автоматизируйте резолв, если обслуживаете много рынков. + +## Роли рядом с вами + +- **Создатель рынка** — выбирает вас оракулом и задаёт окно спора. +- **Беттер** — доверяет вам исход и может оспорить его. +- **Диспутер** — участник спора, который проверяет ваш вердикт. + +Дальше по теме: «Диспуты» (как оспаривается исход и чем это грозит оракулу), «Создатель рынка» (кто и как вас назначает), «Оракул и разрешение» (дедлайны, missed-resolution, no-contest в деталях). diff --git a/@l10n/ru/docs/prediction-markets/guides/passive-lp.md b/@l10n/ru/docs/prediction-markets/guides/passive-lp.md new file mode 100644 index 0000000000..73bcd0317f --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/passive-lp.md @@ -0,0 +1,40 @@ +--- +title: "Пассивный LP — ленивый пул" +description: "Ленивый пул — пассивный продукт: кладёте VIZ, доход от наценки плеча и комиссий. Доли по equity-цене, FIFO-очередь вывода (free_balance ≥ 0 инвариант), штраф только на награды. Пул — контрагент плечевых трейдеров." +--- + +# Пассивный LP: ленивый пул + +Не хотите выбирать конкретный рынок и следить за глубиной — просто положите VIZ в **ленивый пул**. Это пассивный продукт: ваш капитал автоматически питает систему (в первую очередь плечо) и приносит доход, а вы ничего не мониторите. + +## Главное в двух абзацах + +Вы депонируете VIZ (`pm_lazy_deposit`) и получаете **доли** пула по его текущей equity-цене (не по номиналу: цена доли считается от `free + allocated − pending_withdrawals`, чтобы новый вклад при размещённом капитале не получал завышенный вес). Пул выступает **контрагентом плечевых трейдеров**: заём под плечо фронтит именно ленивый пул, а наценка (markup) и funding-rate возвращаются в пул как доход. Плюс к этому — доля общих комиссий. + +Ваш принципал в пуле возвращается, но выплата может встать в **FIFO-очередь**, если свободного баланса не хватает прямо сейчас (капитал размещён в открытых плечевых позициях). Инвариант пула — `free_balance ≥ 0`: пул никогда не платит больше, чем реально свободно; заявка на вывод регистрируется и гасится по мере возврата средств. Штраф при досрочном выводе берётся **только с наград**, принципал не режется. + +## Что происходит по шагам + +**Депозит.** `pm_lazy_deposit` — VIZ уходит в пул, вы получаете доли по equity-цене. Дальше пул сам решает, куда направить капитал (плечо, глубина), вы не управляете этим вручную. + +**Доход капает.** Плечевые трейдеры платят наценку и funding — это идёт в yield пула. Ваши доли растут в стоимости. Доход пассивный, ничего нажимать не нужно. + +**Вывод — плановый или экстренный.** `pm_lazy_withdraw` (частично, по долям, или всё). Если в пуле хватает свободного баланса — выплата мгновенная. Если капитал размещён — заявка встаёт в FIFO-очередь (`pm_lazy_withdraw_request`) и гасится по мере возврата средств из плеча/глубины. Экстренный вывод берёт штраф — но **только с накопленных наград**, ваш принципал не уменьшается. + +**Очередь и порядок.** Заявки гасятся в порядке поступления на каждом возврате свободного баланса (закрытие плеча, конверсия, новый депозит). Это защищает пул от ухода в минус — урок раннего дизайна, когда экстренный вывод мог утащить `free_balance` ниже нуля. + +## Что нужно понимать пассивному LP + +- **Пул — контрагент плеча.** В отличие от прямого LP рынка (глубина кривой, principal-protected от исхода), ленивый пул несёт риск плечевых позиций: его капитал заимствуется трейдерами. Доход выше, но и природа риска другая. +- **Цена доли — по equity, не по номиналу.** Вы получаете доли по реальной стоимости пула, а не 1:1. Это честно распределяет доход между старыми и новыми вкладчиками. +- **Вывод может ждать.** Если весь свободный баланс размещён, ваша заявка встанет в очередь. Это не потеря — принципал вернётся по мере разгрузки; но мгновенность не гарантирована. +- **Штраф — только на награды.** Досрочный/экстренный вывод режет доход, но не принципал. `free_balance ≥ 0` — жёсткий инвариант. +- **Пассивность — плюс и минус.** Вы не выбираете рынки и не мониторите глубину, но и не контролируете, куда пойдёт капитал. + +## Роли рядом с вами + +- **Плечевой трейдер** — заимствует у вашего пула, чтобы поставить на цену; его наценка — ваш доход. +- **Активный LP** — противоположный по духу продукт: ручная глубина конкретного рынка, principal-protected от исхода. +- **Создатель рынка** — формирует рынки, на которых работает плечо и глубина. + +Дальше по теме: «Плечевой трейдер» (кто и как заимствует у пула), «Активный LP» (в чём разница с прямой ликвидностью), «Ленивый пул (детально)» (equity-цена, FIFO-вывод, доходность по шагам). diff --git a/@l10n/ru/docs/prediction-markets/guides/resolution.md b/@l10n/ru/docs/prediction-markets/guides/resolution.md new file mode 100644 index 0000000000..a661a84dc6 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/resolution.md @@ -0,0 +1,49 @@ +--- +title: "Оракул и разрешение — как объявляется исход" +description: "Механизм разрешения рынка: резолв только после закрытия ставок, дедлайн result_expiration, ранний резолв (allow_early_resolution), missed-resolution void после grace, no-contest при отсутствии исхода. Выплаты автоматические." +--- + +# Оракул и разрешение: как объявляется исход + +Это статья про **механизм** разрешения — таймлайн и правила, общие для всех участников. Про обязанности и риски самого оракула — отдельная статья [Оракул](./oracle); здесь мы смотрим, как рынок доходит от закрытия ставок до выплат. + +## Главное в двух абзацах + +Разрешить рынок можно только **после закрытия ставок** (`betting_expiration`) — пока идут ставки, объявлять исход бессмысленно. У рынка есть дедлайн `result_expiration`, к которому оракул обязан объявить результат (`pm_resolve_market`). На резолве выплаты угадавшим начисляются **автоматически** (виртуальная `pm_payout`), проигравшие исходы обнуляются, их деньги идут в призовой пот победителей. + +Если исхода нет — событие отменено, источник пропал, ничья без победителя — рынок закрывается как **no-contest** (`pm_no_contest`): ставки возвращаются, никто не выигрывает и не проигрывает. А если оракул промолчал до дедлайна и грейса — рынок гибнет как **missed-resolution** (оракула штрафуют). Есть и досрочный путь: рынки с `allow_early_resolution` можно закрыть раньше, если исход уже определённо известен. + +## Таймлайн рынка + +**1. Открыт.** Идут ставки, цена плывёт по кривой. Резолв запрещён. + +**2. Закрытие ставок (`betting_expiration`).** Приём ставок прекращён. Начинается окно, в котором оракул может (и должен) объявить исход. + +**3. Разрешение (`pm_resolve_market`).** Оракул объявляет победивший исход. Нода делит пул: выплаты автоматические, забирать вручную не нужно. За резолв оракулу — его fee (ограничен медиан-параметром). + +**4. Окно спора.** После объявления — grace-период (`pm_dispute_grace_sec`), в котором исход можно оспорить (см. [Диспуты](./disputes)). До финализации спора выплаты не окончательны. + +**5. Сеттлмент.** По подтверждённому исходу проходит расчёт: ликвидность LP возвращается (principal-protected), призовой пот распределён. + +## Особые пути + +**Ранний резолв (`allow_early_resolution`).** Если рынок создан с этим флагом и исход уже известен наверняка, оракул закрывает досрочно. При этом `result_expiration` сдвигается к моменту резолва (окно спора схлопывается к «сейчас + grace»), но полный `pm_dispute_grace_sec` от анонса диспутерам сохраняется. Поздний резолв (после `result_expiration`) окно, наоборот, не расширяет. + +**No-contest (`pm_no_contest`).** Нет исхода — рынок отменяется, ставки возвращаются. Это не штраф участникам: их деньги не «сгорают» из-за того, что источник промолчал. Оракул обязан сделать no-contest вовремя, если результата не будет. + +**Missed-resolution (void по дедлайну).** Оракул не объявил исход и не сделал no-contest до `result_expiration` + grace → крон воидит рынок, оракула слэшат. Важно: void срабатывает только **после** `result_expiration + pm_dispute_grace_sec` (тот же cutoff, что settle-sweep) — чтобы у оракула было реальное окно резолва, а не гонка с дедлайном (это чинил фикс reachability, иначе fixed-deadline рынок без early-флага было невозможно резолвить). + +## Что нужно понимать + +- **Резолв — только после закрытия ставок.** Раньше нельзя; ранний путь — отдельный флаг рынка. +- **Выплаты автоматические.** Ни «claim», ни кнопки: выигрыш и возвраты приходят на резолве/отмене. +- **Молчание наказуемо.** Нет исхода → no-contest вовремя. Просто «не сделать ничего» = missed-resolution и слэш оракула. +- **Исход не окончателен до конца грейса.** В окне спора выплаты могут пересчитаться. +- **Плечо резолва не ждёт.** Плечевые позиции закрываются по цене на `betting_expiration`, независимо от вердикта оракула — см. [Плечевой трейдер](./leverage-trader). + +## Роли и связки + +- [Оракул](./oracle) — кто объявляет исход и чем отвечает. +- [Диспуты](./disputes) — как оспорить объявленный результат. +- [Беттер](./bettor) — как выглядит резолв со стороны ставки. +- [Спецификация](../specification) — формальные дедлайны и cutoff-ы. diff --git a/@l10n/ru/docs/prediction-markets/guides/why-pool-not-odds.md b/@l10n/ru/docs/prediction-markets/guides/why-pool-not-odds.md new file mode 100644 index 0000000000..c0b83dd81d --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/guides/why-pool-not-odds.md @@ -0,0 +1,39 @@ +--- +title: "Почему пул, а не коэффициенты" +description: "На VIZ нет фиксированных одды: цену исхода задаёт кривая пула (CPMM для бинарных, LMSR для мульти), а выплата — доля призового пота пропорционально купленным долям. Коэффициент плавает до закрытия ставок." +--- + +# Почему пул, а не «коэффициенты» + +«Какой коэффициент на этот исход?» — самый частый вопрос. На VIZ честный ответ: **фиксированного коэффициента нет**, и это не недоработка, а принцип. Разберём, чем пул отличается от букмекера и как из него получается ваш выигрыш. + +## Главное в двух абзацах + +У букмекера коэффициент называет **контора** — она контрагент, держит риск и зарабатывает на марже. Кто-то должен быть «домом». В блокчейне «дома» нет: иначе это снова посредник, которого экосистема как раз убирает. Поэтому рынок VIZ — это **общий пул**, а цену исходу назначаем мы сами, своими ставками. + +Цена не берётся с потолка и не фиксируется: её задаёт **кривая пула** (CPMM для бинарных рынков, LMSR для мульти-исходных). Каждая ставка двигает цену: чем больше поставили на исход, тем он дороже и тем меньше «долей» (weight) даёт следующий токен. На разрешении призовой пот проигравших делится между угадавшими **пропорционально их долям**. Никакого заранее обещанного «×2.5» — множитель складывается сам из того, как расставились все. + +## Как это работает + +**Цена — состояние кривой сейчас.** Когда вы ставите, нода считает по кривой, сколько долей причитается за вашу сумму по текущей цене. Ранняя ставка на непопулярный исход = дёшево вошли, много долей. Набежала толпа — цена уже другая. + +**Простая интуиция «котла».** Грубо: на «Да» суммарно 100 Ƶ, на «Нет» — 300 Ƶ, случилось «Да». Победители забирают своё и делят проигравшие 300 Ƶ по долям. Ставка 10 Ƶ (десятая часть пула «Да») → ~40 Ƶ на выходе. Точное число даёт кривая (плавная цена вместо ступенек чистого котла), но направление такое: чем меньше народу на победившей стороне, тем жирнее доля. + +**Коэффициент плавает до закрытия.** Раз выплата строится из живых ставок, она меняется, пока рынок открыт. Крупный игрок зашёл на вашу сторону — ваша доля пула уменьшилась, итог будет **ниже**, чем казалось при ставке. Это не обман интерфейса, а природа пула. Поэтому в клиенте — оценка, а не «фикс-коэффициент». + +**Глубина сглаживает.** Ликвидность (прямая от LP + стартовая от создателя) задаёт, насколько резко ставка двигает цену. Глубокий рынок = плавная цена, мелкий = скачки. Ликвидность работает, пока рынок жив, и возвращается на сеттле (principal-protected). + +## Что нужно понимать + +- **Нет «дома» — нет фиксированного коэффициента.** Цену назначают все участники вместе; вы видите текущую оценку, а не обещание. +- **Выплата — доля пота, а не «ставка × коэффициент».** Итог зависит от финального расклада ставок по исходам, а не от числа в момент входа. +- **Ранний вход выгоднее на недооценённом исходе.** Больше долей за токен, пока цена низкая. +- **Не ставьте больше, чем готовы потерять.** Плавающий коэффициент может уйти против вас ещё до закрытия. +- **Параметры — в цепи, не на сайте.** Комиссии, штрафы, лимиты — медиан-голосование делегатов, а не воля конторы. + +## Куда дальше + +- [Беттер](./bettor) — как сделать ставку по шагам. +- [Активный LP](./active-lp) — кто задаёт глубину кривой и почему это principal-protected. +- [Мульти-исходные рынки](./multi-outcome) — когда исходов больше двух (LMSR). +- [Спецификация](../specification) — формальные формулы кривой и сеттлмента. diff --git a/@l10n/ru/docs/prediction-markets/index.md b/@l10n/ru/docs/prediction-markets/index.md new file mode 100644 index 0000000000..64621de0ee --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/index.md @@ -0,0 +1,53 @@ +--- +title: Прогнозные рынки (Onix) — обзор и карта +description: Стек on-chain прогнозных рынков VIZ — протокол Onix и клиент Forecaster — с полным деревом документации от whitepaper до спецификации, объектов, операций, воркфлоу и анализа концептов. +--- + +# Прогнозные рынки на VIZ + +VIZ Ledger исполняет прогнозные рынки как **операции консенсуса первого класса** (`pm_*`), live с HF14. +Два имени, которые встречаются повсюду: + +- **Onix** — **протокол**: on-chain движок рынка (CPMM binary + LMSR multi, parimutuel zero-sum + расчёт, bonded-оракулы, lazy-пул, опциональное плечо, batch / commit-reveal ставки). +- **Forecaster** — **тонкий клиент** к этому протоколу в VIZ Ledger. Это headless, платформонезависимый + фронтенд, позволяющий **людям со всего мира участвовать в on-chain прогнозном рынке** — создавать + рынки, ставить, давать ликвидность, быть оракулом и оспаривать — подписывая операции `pm_*` напрямую + против публичных узлов VIZ. Протокол нейтрален; Forecaster (и любой юрисдикционный клиент, + построенный так же) — это слой доступа. + +## Карта документации + +```mermaid +flowchart TD + ROOT["Прогнозные рынки (Onix)"] + ROOT --> OV["Обзор — одностраничный питч (весь стек кратко)"] + ROOT --> WP["Whitepaper — тезис: почему risk-free LP, два типа рынков, флайвил"] + ROOT --> SP["Спецификация — формальная механика + §17 On-chain модель объектов"] + ROOT --> OPS["Операции — подписанные consensus-операции pm_*"] + ROOT --> VOPS["Виртуальные операции — детерминированные vop на расчёте / по дедлайну"] + ROOT --> API["API плагина — read-методы prediction_market_api"] + ROOT --> WF["Воркфлоу и диаграммы — один канонический рынок через все роли"] + ROOT --> CA["Анализ концептов — 90 концептов PM-теории против живой реализации на VIZ"] +``` + +## С чего начать + +| Страница | Что это | +|------|-----------| +| [Обзор](./onix) | Одностраничное позиционирование: parimutuel с AMM-ценой и структурно безрисковой ликвидностью. | +| [Whitepaper](./whitepaper) | Индустриальный тезис — гарантия LP, Onix Binary (CPMM) + Onix Multi (LMSR), оракулы, lazy-пул, плечо, управление. | +| [Спецификация](./specification) | Формальная спека: параметры, машина состояний, ценообразование, расчёт, споры, lazy-пул, плечо и **[On-chain модель объектов](./specification)** (каждый `pm_*_object` и его индекс поиска). | +| [Операции](../protocol/operations/prediction-markets) | 21 подписанная consensus-операция (`pm_create_market`, `pm_place_bet`, …). | +| [Виртуальные операции](../protocol/virtual-operations) | Детерминированные vop (`pm_payout`, `pm_market_accepted`, `pm_leverage_resolve`, `pm_batch_settle`, …). | +| [API плагина](../plugins/prediction-market-api) | `prediction_market_api` — read-доступ к рынкам, ставкам, оракулам, спорам, lazy-пулу и медиана-голосуемым параметрам. | +| [Воркфлоу и диаграммы](./workflows) | Один канонический бинарный рынок через всех участников, с zero-sum мастер-леджером для нормального и спорного разрешения. | +| [Анализ концептов (Onix против 90)](./concepts-analysis) | Как on-chain реализация ложится на атлас теории прогнозных рынков — что решено, имманентно, не нужно или в роадмапе. | + +## Управление + +Все экономические параметры делегат-**медиана-голосуемы** и живут в структуре `chain_properties_pm` — +см. [Параметры цепи → Параметры прогнозных рынков](../governance/chain-properties#pm-parameters). +Хардфорк для настройки комиссий, штрафов, lazy-пула, плеча или тайминга batch/commit-reveal не нужен; три +живых kill-switch (`pm_commit_reveal_enabled`, `pm_lazy_pool_enabled`, `pm_leverage_enabled`) позволяют +медиане валидаторов отключить целую подсистему без форка. diff --git a/@l10n/ru/docs/prediction-markets/onix.md b/@l10n/ru/docs/prediction-markets/onix.md new file mode 100644 index 0000000000..118329978b --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/onix.md @@ -0,0 +1,89 @@ +--- +title: Onix — прогнозные рынки с AMM-ценой и тотализаторными выплатами +description: "Onix кладёт непрерывное AMM-ценообразование поверх parimutuel-выплат — цена движется как в AMM, а ликвидность несёт риск-профиль тотализатора: маркет-мейкера нельзя обанкротить." +--- + +# Onix — прогнозные рынки: AMM-цена, тотализаторные выплаты + +> Непрерывное **AMM-ценообразование** поверх **parimutuel-выплат** — цена движется как в AMM, а +> ликвидность несёт риск-профиль тотализатора: **маркет-мейкера нельзя обанкротить.** + +::: info Onix и Forecaster +**Onix** — это on-chain протокол. **Forecaster** — тонкий клиент к нему в VIZ Ledger: headless, +платформонезависимый слой доступа, позволяющий людям со всего мира участвовать в on-chain прогнозном +рынке, подписывая операции `pm_*` напрямую против публичных узлов VIZ. Полное дерево документации — +[обзор и карта раздела](./). +::: + +## Одна идея + +Onix **разделяет цену и выплату**: + +- **Цена (discovery)** — кривая CPMM (binary) или LMSR-softmax (multi) обновляет живую вероятность на + каждой ставке и присваивает ставке **вес** (её «билет» на долю). +- **Выплата (settlement)** — победителям платят **только** из проигранных ставок, делёж по весу: чистый + **parimutuel**, строго zero-sum (протокол никогда не печатает токен). + +Всё отличительное в Onix вытекает из этого разделения. + +## Почему это важно — три вещи + +::: tip 1 · Ликвидность, которую нельзя «слить» +Поскольку победителям платят из проигравших, а не из принципала LP, поставщика ликвидности **нельзя +обанкротить** — нет impermanent loss, нет инвентарного риска, нет «смерти от снайпера». Гарантировано *by +construction* (AM–GM для CPMM, сохранение для LMSR), а не страховкой. +::: + +::: tip 2 · Пассивный доход без IL — Lazy Pool +Один депозит сам растекается тихой ликвидностью по многим рынкам и фондирует опциональное плечо, учёт +наград в стиле MasterChef. Доход от ликвидности ПМ **без** выбора рынков и без impermanent loss. +::: + +::: tip 3 · Нативность к цепи, zero-sum +Рынки — это операции консенсуса первого класса (`pm_*`), а не смарт-контракты: цензуроустойчиво, +композируемо, блоки ~3 секунды, без оракул-моста. Протокол не эмитит токены — только перераспределяет. +::: + +## Как работает ставка + +1. **Ты ставишь** `X` на исход. `X` входит в кривую; кривая возвращает твой **вес** — больше веса, если + зашёл раньше, до сдвига цены. +2. **Борд обновляется.** Живой коэффициент стороны = `1 + чужой_пул × (1 − комиссия) / свой_пул`, причём + комиссия (оракул + создатель + LP) уже зашита. +3. **На резолюции** ставки проигравших (минус комиссия) делятся между победителями по весу. Твоя выплата + = возврат ставки **+** твоя доля проигравшего пула. Принципал LP возвращается нетронутым. + +## Сравнение + +| | CLOB / AMM (Polymarket, Kalshi) | Голый parimutuel (тотализатор) | **Onix** | +|---|---|---|---| +| Живая цена | да | нет (только соотношение пулов) | **да (CPMM / LMSR)** | +| Кэф зафиксирован при ставке | да | нет | нет (честный parimutuel) | +| LP / мейкер может обанкротиться | **да** (IL, снайперы, gap) | n/a | **нет (структурно)** | +| Доходный слой ликвидности | хрупкий | нет | **Lazy Pool, без IL** | +| Где живёт | контракты / бэкенд | бэкенд | **консенсус (`pm_*`)** | +| Эмиссия токенов | бывает | нет | **нет (zero-sum)** | + +## Честный компромисс + +::: warning Кэфы — parimutuel: они плывут до закрытия +Onix **не** фиксирует твой коэффициент в момент ставки. Борд движется по мере притока денег, и финальный +кэф известен только на закрытии — ровно как в тотализаторе. Это не баг для заплатки: *единственный* способ +зафиксировать кэф — чтобы риск нёс контрагент (букмекер или AMM-LP, который может потерять). Drift в Onix — +прямая цена его LP-гарантии: риск живёт **между беттерами**, поэтому ничья ликвидность не может сгореть. +::: + +## Что новаторского + +- **AMM-вес + parimutuel-сеттлмент** в одном движке — непрерывный price discovery *без* инвентарного + риска мейкера. +- **Структурная, доказуемая безопасность LP** вместо застрахованной или субсидируемой ликвидности. +- **Мьютуализированный доходный слой ликвидности** (Lazy Pool), который ещё и фондирует опциональное + плечо — ликвидация идёт по pre-bet резервам, поэтому пул всегда остаётся целым. +- **Опциональный анти-MEV** (batch / commit-reveal ставки) и **прозрачное управление** (bonded-оракулы, + публичные слушания споров с изменяемыми голосами) — всё поверх безопасной базы, не трогая LP-гарантию. + +## Подробнее + +- Операции протокола — [Прогнозные рынки](../protocol/operations/prediction-markets) +- API плагина — [API прогнозных рынков](../plugins/prediction-market-api) diff --git a/@l10n/ru/docs/prediction-markets/parlay-spec.md b/@l10n/ru/docs/prediction-markets/parlay-spec.md new file mode 100644 index 0000000000..b067489ea0 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/parlay-spec.md @@ -0,0 +1,263 @@ +# 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`, 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 после + запуска в любом случае, дефолты только сидят самую первую медиану. diff --git a/@l10n/ru/docs/prediction-markets/settlement-work-bounds.md b/@l10n/ru/docs/prediction-markets/settlement-work-bounds.md new file mode 100644 index 0000000000..1098e71f5f --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/settlement-work-bounds.md @@ -0,0 +1,556 @@ +# Ограничение работы сеттлмента за блок (#432) + +Статус: фикс **A** реализован (default подтверждён 1.000 VIZ) и фикс **D** завершён — garbage +collection, сеттлмент и void-рефанды работают на метрированном row-бюджете. Эта заметка фиксирует +проблему, взвешенные варианты и почему цепь берёт оба. + +Соседние внутренние спеки: [early-exit-deferred-claim](./early-exit-deferred-claim.md), +[specification](./specification.md) §5 (crons). + +## 1. Дыра + +`pm_processing_cap_per_block` (median-voted, default 200) — единственный ограничитель PM-крона. +Он считает **рынки**, а не работу: + +``` +while (it != idx.end() && ... && done < cap) { // §5 auto-payouts + settle_market(*this, mkt); // трогает КАЖДУЮ строку ставки рынка + ++done; // ...и стоит ровно ОДНУ единицу cap +} +``` + +`settle_market()` (`libraries/chain/pm_process_markets.cpp`) обходит весь `by_market`-диапазон +ставок, строит векторы победителей/проигравших, платит каждую строку через `adjust_balance`, +флипает её статус и пушит одну virtual op на строку. Нет курсора и нет resume: рынок сеттлится +целиком внутри одного блока или никак. Та же форма в `gc_market()` (сносит весь кластер объектов +рынка за один блок) и в void/no-contest-ветке. + +Ничто не ограничивало число строк, которое может нести рынок: + +* каждый `pm_place_bet` создаёт **новый** `pm_bet_object` — нет агрегации по + (account, market, outcome); +* у instant-пути **вообще не было минимума ставки** (`amount > 0` и `tokens_out > 0` были + единственными гейтами), поэтому строка стоила 1 raw = **0.001 VIZ**. Проверено живым бродкастом + на тестнете, с reject-контролем и accept-контролем, на 0.001 / 0.002 / 0.010 VIZ; +* **частичный** `pm_transfer_position` делит одну строку на две вообще без стоимости стейка — + дешевле, чем беттинг, и обходит любой bet-side пол; +* тот же класс cap уже существовал повсюду — `MAX_PM_DEFERRED_CLAIMS_PER_MARKET`, + `MAX_PM_DISPUTE_VOTES_PER_MARKET`, `MAX_PM_OPEN_COMMITS_PER_MARKET`, все по 10 000. Bet-строки + были единственным членом класса, оставленным открытым. + +Для этого не нужен атакующий. Достаточно **популярного** рынка: на тестнете игрушечный бот, +ставящий с трёх аккаунтов каждые десять минут, уже накопил 734 строки на одном рынке (571 и 552 на +двух других). Мейннет-рынок с тысячами участников на порядки больше, и вся эта работа ложится в +один блок, где истекает dispute grace. + +## 2. Варианты + +| | Фикс | Ограничивает работу? | Цена | +|---|---|---|---| +| A | минимум ставки на instant-пути (зеркало `pm_min_batch_bet`) | нет — только ценит строки | один assert, median-tunable | +| B | жёсткий кап на строки per market | да | популярный рынок перестаёт принимать ставки: цензура / сломанный UX | +| C | агрегировать ставки по (account, market, outcome) | ограничен аккаунтами | инвазивно: ломает per-bet `weight`, `time_penalty`, `entry_liquidity`, transfer_position, F1 claims | +| D | инкрементальный сеттлмент: ограниченные строки на блок, курсор на рынке | да | самый большой consensus-дифф | + +**Выбрано: A + D.** + +* **A один — не фикс.** Он поднимает цену строки на три порядка (0.001 → 1.000 VIZ) и он votable, + но даёт экономическую, а не структурную границу: 1 000 000 строк по 1 VIZ = 1 000 000 VIZ, это + много денег, но не невозможная сумма — притом она *застейкана*, а не потрачена, так что большая + доля вернётся на выплате. Важнее: A ничего не делает с легитимным случаем — действительно + популярный рынок это не спам и не должен наказываться, но это та же блок-тайм проблема. +* **D один тоже недостаточен.** Он делает работу за блок конечной, но оставляет строки бесплатными, + так что спамер всё равно может растянуть сеттлмент одного рынка на тысячи блоков и заставить + каждую ноду нести state. A — дешёвый экономический гард, который держит очередь D короткой. +* **B отвергнут**: отказ в ставках на рынке, который хорошо идёт, — видимый пользователю провал + продукта, а значение капа пришлось бы угадывать. +* **C отвергнут**: это более широкий и рискованный дифф, чем D, за ту же выгоду, и он уничтожает + per-bet свойства, от которых зависят сеттлмент-математика и early-exit claims. + +## 3. Фикс A — минимум ставки (реализован) + +В `chain_properties_pm` добавлены два median-voted параметра: + +* `pm_min_bet` — default `1.000 VIZ`, governance floor `0.1 VIZ` (`validate()`), зеркало + `pm_min_batch_bet`; +* `pm_settle_rows_per_block` — default `2000`, диапазон `[100, 100000]`, потребляется фиксом D. + +Оба вплетены в median loop в `database.cpp`. PM-параметр, который объявлен, отражён и провалидирован, +но никогда не входит в этот loop, молча un-votable и заморожен на code-default — такое уже случилось +один раз с `pm_closed_market_retention_sec`. + +Точки enforcement (`libraries/chain/pm_evaluator.cpp`): + +1. `pm_place_bet`, `mode == 0` (instant) → `amount >= pm_min_bet`; +2. `pm_place_bet`, `mode == 1` (queued batch) → `amount >= pm_min_batch_bet`. Этот путь тоже создаёт + строки и тоже не имел пола — покрыт был только *commit*-путь; +3. `pm_transfer_position`, частичный → **и** переданная часть, **и** остаток должны оставаться на + уровне `pm_min_bet` или выше. Позиция ниже пола не заперта: её всё ещё можно передать целиком, + что двигает строку вместо её деления. + +4. `pm_add_liquidity` → `pm_min_liquidity`. Ликвидность — четвёртый источник строк и тот, что + изначально ускользнул: каждый вызов минтит свой `pm_liquidity_object` (взносы не сливаются по + провайдеру), а эвалюатор ассертил только `amount > 0`, поэтому строки можно было минтить по + 1 raw за штуку, пока bet-пути были полованы. Пол тот же, что уже гейтит создание рынка, так что + минимальный билет для внесения ликвидности не зависит от того, открываешь ты рынок или + докладываешь позже — продуктовое решение, принятое осознанно, а не по умолчанию. + +## 4. Фикс D — инкрементальный сеттлмент (дизайн) + +Рынок несёт собственный курсор сеттлмента, а крон тратит **глобальный** per-block row-бюджет +(`pm_settle_rows_per_block`, общий для всех сеттлящихся рынков, самые старые рынки первыми). +`settle_market` становится phase-machine, возобновляемой блок за блоком: + +| фаза | работа на строку | бюджетируется | +|---|---|---| +| 1 force-close | закрыть плечевые позиции, ещё открытые на сеттле | нет — см. ниже | +| 2 aggregate | рефанд queued-строк (status 5/6), суммировать `losers_sum` и вес победителей | да | +| 3 claims | оплатить outcome-contingent early-exit claims из ограниченного ведра | да | +| 4 payout | заплатить победителям / флипнуть проигравших, одна virtual op на строку | да | +| 5 finalize | комиссии, сеттл LP, пыль, `payout_status = 3`, `finalized_time` | нет — см. ниже | + +Метрируется только bet-проход, потому что только bet-строка дешёвая. Фазы 1 и 5 итерируют плечевые +позиции и строки ликвидности, а каждая из *них* стоит `pm_min_liquidity` (100 VIZ) при создании — +в сто раз дороже bet-строки. Их работа ограничена экономически, тем, что атакующему пришлось бы +застейкать, чтобы создать строки, поэтому метрирование добавило бы курсоры и resume-state ради +угрозы, которую фикс A уже выценил. Если это когда-нибудь изменится (появится дешёвый способ +минтить LP или leverage-строку), эти фазы потребуют той же обработки и той же дисциплины `escrow`. + +#### Где живёт resume-state + +Проход текущего `settle_market()` от начала до конца даёт точный state, который должен нести +приостановленный сеттлмент, и это больше, чем курсор: сплит денег — двухпроходный алгоритм. Проход +один производит агрегаты (`losers_sum`, суммарный вес победителей), проход два превращает их в +per-row выплаты. Разрежь функцию на любой границе блока — обоим проходам нужны сохранённые частичные +результаты, плюс running totals, которые нужны финализации для пыли. + +Этот state **не** вешается на `pm_market_object`. Это двенадцать полей, которые несли бы каждый +рынок, когда-либо существовавший, при том что использовать их могут лишь единицы сейчас +сеттлящихся — объект уже ~40 полей в ширину, а рынки — самый многочисленный объект на цепи. Вместо +этого один `pm_settlement_object` создаётся при входе рынка в сеттлмент, ключуется уникально по +рынку и удаляется на финализации, так что стоимость пропорциональна сеттлментам *в полёте*: + +| поле | фаза | смысл | +|---|---|---| +| `phase`, `cursor` | все | текущая фаза и следующий id строки для обработки в ней | +| `stake_total` | 2 | `losers_sum` (норма) / суммарный активный стейк (void) | +| `weight_total` | 2 | Σ curve-вес победителей, 128-бит как в `compute_settlement` | +| `winners_pool`, `uncovered` | ставятся на 3→4 | константы сплита, когда claims финальны | +| `distributed` | 4 | Σ выплаченной прибыли — финализация роутит `winners_pool − distributed` как пыль | +| `lp_bonus` | 4 | Σ time-penalty, снятого с победителей | +| `paid_claims` | 3 | взято из ограниченного early-exit ведра | +| `escrow` | все | знаковый аккумулятор консервации (ниже) | + +Per-winner выплата зависит только от `winners_pool`, `weight_total` и собственных полей строки, +поэтому фазе 4 не нужна память о строках, которые она уже оплатила — именно это делает разрез +чистым. Void-путь переиспользует те же поля (его два pro-rata распределения накапливаются в +`distributed` и `lp_bonus`), сохраняя округление «последний участник забирает остаток» идентичным +сегодняшнему. + +Поскольку объект удаляется на финализации, у рынка, который *не* сеттлится, строки сеттлмента нет +вовсе, и garbage collection сносит его вместе с остальным кластером. + +Правила, которые делают это безопасным: + +* **Детерминизм.** Фаза, курсор и аккумуляторы живут в объекте сеттлмента; бюджет — median-voted + параметр. Каждая нода, следовательно, обрабатывает ровно те же строки в тех же блоках. +* **Рынок закрыт для всего остального, пока сеттлится.** `payout_status = 4` («settling») не даёт + §5 войти повторно, не пускает `pm_dispute_create` (он требует `payout_status == 1`), а GC не может + сработать, потому что `finalized_time` штампуется только в фазе 5. +* **Консервация на каждой границе блока.** Деньги, освобождённые со строки, но ещё не выплаченные, + держатся в явном аккумуляторе `escrow`: `+= amount` при освобождении строки, `-= payout` при оплате. + PM supply-инвариант считает их PM-held, так что снапшот, снятый посреди сеттлмента, балансируется + точно; финализация ассертит достижение нуля. Аккумулятор **знаковый**: фаза 3 платит early-exit + claims из проигравшего пула, чьи строки ещё стоят, поэтому он легитимно уходит в минус до того, + как фаза 4 их освободит. Это не дефицит — токены в реальных балансах аккаунтов, а строки, которые + их профинансируют, всё ещё считаются PM-held, так что обе стороны инварианта двигаются вместе в + любую сторону. +* **Прогресс.** Рынок с N строками финиширует примерно за N / budget блоков; пол 100 на бюджете + делает голодание невозможным. +* **Сами строки не могут двигаться.** Каждая операция, создающая, делящая или удаляющая bet-строку — + `pm_place_bet`, `pm_commit_bet`, `pm_reveal_bet`, `pm_cancel_bet`, `pm_transfer_position` — + ассертит `mkt.status == 1`, а сеттлящийся рынок на status 3. Поэтому набор, пройденный в фазе 2, + ровно тот же, что оплачен в фазе 4, без добавления гейта. + +Одно cross-section взаимодействие **не** держится автоматически, и реализация должна его закрыть: +крон §1 (forfeit коммитов, которые так и не раскрыли) добавляет штраф в `forfeit_pool` *какого +угодно* рынка, которому принадлежит коммит, не глядя на его статус. Сегодня это безвредно — §1 +работает раньше в том же блоке, чем §5, так что сеттлмент читает финальный `forfeit_pool` — но +сеттлмент, растянутый на блоки, может получить `forfeit_pool`, выросший *после* того, как фаза 3 +уже свернула его в `winners_pool`, и эти токены тогда не принадлежат никому (осиротели до GC-сжигания, +т.е. снова failure-режим drift-400). Тайминг делает это маловероятным на практике — reveal-дедлайн +сидит на закрытии беттинга, задолго до `result_expiration + grace` — но «маловероятно на практике» +ровно то рассуждение, которое породило прежние drift'ы. Фаза 5 поэтому должна роутить любой +`forfeit_pool`, появившийся в полёте, вместо предположения о нуле, а escrow-ассерт на финализации +должен это учитывать. + +### 4.1 Поставлено: ограниченный garbage collection + +Collection пошёл первым — это тот же unbounded-проход без сеттлмент-арифметики, так что он +валидирует бюджетную сантехнику сам по себе. `gc_market()` стал `gc_market_step(db, mkt, budget)`: +сносит не более `budget` объектов, декрементит его на месте и возвращает true только когда весь +кластер (включая объект рынка) исчез. Рынок, слишком большой для одного блока, сохраняет своё место +в голове свипа — `finalized_time` никогда не меняется — и продолжает на следующем блоке. + +Курсор не нужен, в отличие от сеттлмента: каждый диапазон входится заново на своём `lower_bound`, а +уже удалённые строки *исчезли*, так что свип возобновляется ровно там, где остановился. Две детали +делают паузу безопасной: + +* сжигание `forfeit_pool` теперь обнуляется в том же шаге, иначе повторный вход сжигал бы те же + токены заново на каждом блоке и толкал `current_supply` ниже учтённой суммы; +* наполовину собранный рынок инертен — терминальный (`status 3` / `payout_status 3`), так что ни + одна операция не может до него дотянуться, а сносимые строки не держат денег (ставки `2/3`, LP + `3`, плечо терминальное), так что supply-инвариант плоский поперёк паузы. + +Покрыто `gc_row_budget_spans_blocks` (consensus_sim): кластер в 143 строки с бюджетом на его полу +100 должен занять больше одного блока и никогда не терять больше 100 строк за блок. Проверено +против намеренно unbounded-контроля — с обойдённым бюджетом тот же тест сообщает «один блок удалил +143 строки» и «собран за 1 блок», т.е. до-фиксовое поведение. + +Сеттлмент обслуживается раньше collection в блоке, так что тяжёлый сеттлмент-бэклог может отложить +GC. Это безвредно: лишь растягивает retention, а сеттлмент конечен. + +### 4.2 Поставлено: инкрементальный сеттлмент + +`settle_market()` стал `settle_market_step(db, mkt, budget)`, управляется `pm_settlement_object`, +описанным выше, и тратит тот же глобальный row-бюджет, что и collection. Три вещи, которые +реализация должна была сделать правильно, ни одна не видна из скетча дизайна: + +* **«Я последняя строка?» нельзя ответить, заглядывая вперёд.** Сегодняшний код отдаёт остаток + округления последнему участнику, которого опознаёт, подглядывая в остаток рынка. Под resume этот + подгляд и неверен (уже оплаченные строки всё ещё в диапазоне, просто терминальные), и + квадратичен. Фаза 2 поэтому считает агрегируемые строки в `rows_total`, а фаза 4 считает + оплаченное в `rows_done`; последняя строка — это `rows_done == rows_total`, за O(1) и стабильно + поперёк паузы. +* **Void-ветка должна освобождать то, что сжигает.** Когда у void-рынка не осталось участников, + чтобы поглотить остаточные пулы, остаток сжигается — и сжигание без вычитания из `escrow` + триггерит финализация-ассерт. Учёт консервации должен покрывать путь уничтожения, а не только + пути оплаты. +* **Per-block работа не должна повторять one-shot side effects.** Workload-гейдж + `markets_in_dispute_window` декрементился на call-site, который теперь работает на каждом блоке + полёта; он переехал в ветку, которая выполняется один раз, когда рынок впервые входит в сеттлмент + (`payout_status != 4`). Тот же класс бага, что у сжигания `forfeit_pool` в collection. + +Покрыто `settle_row_budget_spans_blocks` (consensus_sim): рынок в 140 строк сеттлится с бюджетом на +его полу 100, так что должен занять больше одного блока, никогда не терминировать больше 100 строк +в блоке, показывать `payout_status = 4` в полёте и закончить с каждой строкой оплаченной, объектом +сеттлмента снесённым и балансами бетторов, сдвинутыми ровно на сумму, записанную строками. +Проверено против намеренно unbounded-контроля — с обойдённым бюджетом тот же тест сообщает «один +блок оплатил 140 строк» и «сеттлился за 1 блок», т.е. до-фиксовое поведение. + +### 4.3 Поставлено: инкрементальные void-рефанды + +Два void-пути — крон §2 (missed resolution) и §3 (dispute auto-close) — имели ту же дыру со вторым +ребром: `refund_all_bets()` обходил каждую строку рынка за один блок *и* строил in-memory вектор +всех участников, потому что forfeit-пул делится pro-rata, а знаменатель известен только в конце +прохода. + +`refund_market_step(db, mkt, budget)` заменяет оба. Он выполняет два метрированных прохода по +одному предикату (`status` 0/5/6): проход один только меряет (сумма стейков, число строк), проход +два возвращает стейк и платит каждой строке её долю forfeit-пула. Поскольку проход один ничего не +меняет, проход два переобходит *ровно* тот набор, что посчитал проход один — так «кого рефандит +этот void» остаётся ответимым поперёк паузы без меток строк или удержания вектора. + +Рынок носит `payout_status = 4` с первого блока полёта, и этот флаг теперь гейт, а не просто +display-значение: + +* `pm_resolve_market`, `pm_no_contest` и `pm_transfer_position` отказывают ему, так что поздний + оракул-вызов не может обогнать рефанд, прошедший половину рынка; +* крон §4 (финализация голосования диспута) перешагивает диспуты, чей рынок уже воидится §3; +* крон §6 (batch epoch settle) пропускает его, так что queued-строки не могут сдвинуться между + двумя проходами. + +Один баг порядка выпал из написания этого, и он предшествует изменению: старый путь сливал +`forfeit_pool` *до* `return_liquidity()`, который force-закрывает плечевые позиции и роутит их +curve-residual прямиком обратно **в** `forfeit_pool`. Эти токены затем ехали на строке рынка, пока +GC её не сносил — stranded в `current_supply` без владельца, ровно та утечка, которую void-роутинг +существует предотвращать. Ликвидность теперь возвращается первой, а остаток учитывается после. + +Покрыто `void_refund_row_budget_spans_blocks`: 140 строк, бюджет на его полу 100, никакой оракул не +резолвит; void должен растянуться на блоки, оставаться под бюджетом за блок, закончить с +`status = 3`, `resolved_outcome = -1`, каждой строкой отрефанденной и балансами бетторов, +поднятыми ровно на стейк. + +### 4.4 Батч-исполнитель (крон §6) + +Крон §6 заполнял каждую строку, поставленную в очередь текущей эпохи рынка, внутри одного блока, по +цене одной единицы считающего рынки капа — та же форма, что у бага сеттлмента, а после фикса A +queued-строка стоит `pm_min_bet` (1 VIZ), ровно столько же, сколько bet-строка. На LMSR-рынке каждая +строка дополнительно платит за котировку кривой, что делает её *дороже* на строку, чем сеттлмент. + +Теперь он черпает из того же общего `pm_settle_rows_per_block`-бюджета, чарджится за каждую +**посещённую** строку (не только исполненную — визит это работа, которую делает блок). Два следствия +вытекают из того, что оставшиеся строки матчатся *по эпохе*: + +* счётчик эпохи инкрементится только когда очередь вычерпана; бамп посреди слива оставил бы + оставшиеся строки недостижимыми с уже списанным стейком; +* исполнитель поэтому также работает **вне** границы эпохи, пока проход в полёте + (`pm_batch_settle_bet_cursor != 0`), вместо того чтобы заставлять уже раскрытые стейки ждать + целое окно эпохи до следующей границы. + +Точка resume — второй курсор в dynamic global properties, `pm_batch_settle_bet_cursor`, потребляемый +первым рынком, который посещает round-robin скан (это `pm_batch_settle_cursor` by construction). +Потеря его — например, старый снапшот без поля — безопасна: проход рестартует с головы эпохи и +пропускает уже исполненные строки по статусу, ценой одного idle-прохода и нуля денег. Покрыто +`batch_queue_row_budget_spans_blocks`. + +Один гард несущий, а не косметический: §6 входится только при `row_budget > 0`. Он работает +последним, а бюджет общий, так что settle-тяжёлый блок может дойти до него с нулём; войдя всё равно, +он выполнил бы ноль итераций и провалился в persist-шаг, который — не видя mid-market-остановки — +записал бы нулевой курсор строки поверх припаркованного. Следующий проход рестартовал бы с головы +эпохи и потратил бюджет на повторное посещение уже исполненных строк. + +### 4.5 Deadline-свипы перечитывали settled-рынки (найдено 2026-08-19, исправлено) + +`by_result_expiration` был ключован `(status, result_expiration, id)`. Settled-рынок держит +`status == 3`, а его `result_expiration` остаётся в прошлом, так что он сидел в **голове** диапазона, +который проходит §5 settle-свип — и скип его не стоит `cap`, так что цикл никогда не останавливался +на нём рано. Каждый блок поэтому перечитывал весь settled-бэклог до реальной работы: замерено на +тестнет-снапшоте блока 82641602 — **48 971 итерация, из них 48 942 чистых `continue`**, при только +29 рынках, реально ждущих сеттлмента. Бэклог ограничен GC-retention (default 5 д), так что это не +утечка — но он пропорционален обороту, и атакующий может раздуть его напрямую, создавая и резолвя +рынки. + +Фикс ключует индекс `(status, finalized_time, result_expiration, id)`. `finalized_time` штампуется +ровно один раз, на финализации, так что `finalized_time == 0` означает «ещё должен работу»; оба +свипа (§2 missed resolution, §5 settle) делают `lower_bound` в эту группу, и рынок покидает её в +момент сеттла. `payout_status` намеренно *не* в ключе — settle-свип флипает его 1 → 4 в полёте и не +должен двигать строку, которую продолжает. Тот же трюк, что у `by_oracle_finalized`. Ключи индексов +не сериализуются, так что миграция снапшота не нужна. + +### 4.6 Тэлли диспута (крон §4, найдено 2026-08-19, исправлено) + +Та же форма ещё раз, в том единственном свипе, куда прошлые проходы не заглядывали. Крон §4 +финализирует диспуты, чьё окно голосования закрылось; для каждого обходит **каждый бюллетень** +спорного рынка, чтобы построить stake-weighted тэлли, и чарджит рынку одну единицу считающего рынки +`cap`. Бюллетень тоже не дешёвая строка — каждый стоит account lookup плюс lazy-pool deposit lookup, +того же порядка, что и строка сеттлмента, замеренная в §5 ниже. + +M3 уже капает бюллетени на `MAX_PM_DISPUTE_VOTES_PER_MARKET` (10 000) на рынок, и коммент там +рассуждал, что это делает финализация-проход безопасным. Не делает: кап ограничивает *один* рынок, +а §4 может финализировать `cap` таких в блоке, так что потолок был `cap × 10 000` = 2 000 000 строк — +на три порядка выше бюджета, который теперь уважает любой другой свип. Заполнить его медленно +(бюллетеню нужен отдельный аккаунт на рынок, а 200 диспутов стоят 200 × `pm_dispute_fee` в escrow), +но бюллетени — durable state: стоимость размазана по часам chain-времени, а работа реплеится в одном +блоке, где истекают окна голосования. + +В отличие от сеттлмента, тэлли **нельзя** возобновить: вердикту нужны все бюллетени сразу, а +парковка частичных per-outcome сумм означала бы держать вектор на строке диспута. Поэтому бюджет +enforced *между* диспутами — диспут стартует только пока остался бюджет, а затем чарджится за +пройденные бюллетени. Worst case за блок становится `row_budget` + кап бюллетеней одного рынка вместо +`cap` × кап бюллетеней. Отложить финализацию на блок экономически инертно: `pm_dispute_vote` отвергает +бюллетени после `voting_end_time`, так что электорат уже финален, когда §4 добирается. Покрыто +`dispute_tally_row_budget_defers_next`. + +Свойство порядка, общее у этого с §5 и §6, стоит проговорить один раз: бюджет тратится в порядке +секций, так что блок, насыщенный void-путями, может не оставить ничего свипам позади. Это намеренно +— бэклоги это конечная работа, которая сливается — но значит, что «когда финализируется мой диспут» +ограничено *суммарной* PM-работой в полёте, а не только §4. + +Связанная per-transaction стоимость пофикшена рядом. `pm_dispute_vote` раньше enforcing кап +бюллетеней пересчётом существующих бюллетеней рынка на каждый *новый* бюллетень (проход ограничен +cap+1): ограниченно на транзакцию, но O(n) на бюллетень, O(n²) на заполнение рынка, и работа, которую +не покрывает никакой бюджет крона — тот же антипаттерн, что M4 убрал из commit-пути с `open_commits`. +Счёт теперь живёт на `pm_dispute_object.ballots`, инкрементится при создании строки бюллетеня и не +трогается, когда голосующий *ревизует* её (ревизия перезаписывает строку, так что счётчик считает +строки, а не голоса). Бюллетени никогда не удаляются по одному — GC сносит весь кластер — так что +счётчик только растёт. + +Снапшотам здесь нужен один лишний шаг, которого не было у `open_commits`. Диспуты импортируются +*раньше* своих бюллетеней, так что `contains`-гардованное чтение ключа не может само починить +pre-field снапшот: `reconcile_pm_dispute_ballots()` выполняется после импорта бюллетеней и приводит +каждый счётчик в согласие с реально присутствующими строками. Это и сидит старые снапшоты (ключ +отсутствует → 0 → перестроено), и ловит drift в новых, ценой одного прохода по индексу, который импорт +и так только что прошёл. Покрыто `dispute_ballot_counter_matches_rows`, который сверяет счётчик с +живым числом строк после каждого бюллетеня и пинит путь ревизии. + +### 4.7 Порядок секций — это порядок приоритета (крон §8, найдено 2026-08-19, исправлено) + +Каждая секция `process_pm_markets()` чарджит один и тот же счётчик `done` против одного +`pm_processing_cap_per_block`. Это делает порядок секций порядком приоритета, что и задумано для +свипов, делающих реальную работу — но это также значит, что секция, которая надёжно выедает бюджет, +превращает всё за собой в мёртвый код. + +Секция 7, шаг recall ленивого пула, ровно такая секция. Она проходит индекс status-0 аллокаций с +головы каждый блок и чарджит `done` за **каждую посещённую строку**, включая те, что только +инспектирует и оставляет нетронутыми (`idle, steps remain, but this step isn't due yet`). Чардж за +инспекцию намеренный — это то, что держит секцию ограниченной — но рабочий набор велик и долгоживущ: +на тестнете на блоке 82646702 было **34 548** status-0 аллокаций против капа **200**. Цикл поэтому +всегда крутится до `done == cap`. + +За ней сидела секция 8, свип истечения банов. Он не исполнялся никогда. Временные баны оракулов и +создателей держали стухший `banned_until` вечно, и `pm_ban_expired` никогда не эмитился. Ущерб +ограничен: enforcement сравнивает `banned_until` с `now`, а не тестирует поле на пустоту, так что +ни один аккаунт не оставался заблокированным дольше срока — сломалось хранимое состояние и событие +истории, и любой клиент, читающий «забанен» как «поле непустое». На тестнете банов не было, пока это +было правдой, так что ничего наблюдаемо не залипало; дефект в том, что секция вообще не могла +отработать. + +Фикс даёт свипу собственный счётчик (`ban_done`), а не переносит его или раздувает общий кап. Это +безопасно, потому что свип самоочищающийся: визит ставит `banned_until` в 0, что навсегда убирает +строку из свипаемого диапазона. Per-block работа поэтому — число банов, которые только что истекли, +а приватный кап ограничивает даже синхронный залп. + +`ban_expiry_survives_saturated_cron_budget` воспроизводит голодание в миниатюре — кап 2, три живые +аллокации для насыщения, один короткий бан создателя, который всё равно должен истечь. Со свипом +обратно на общем счётчике тест падает ровно на этом ассерте. + +Общее правило, которое это оставляет: **новая секция, добавленная в этот крон, мертва по прибытии, +если она не сидит перед секцией 7 или не несёт собственный бюджет.** + +### 4.8 Очередь вывода ленивого пула (per-tx, найдено 2026-08-19, исправлено 2026-08-20) + +`service_lazy_withdraw_queue()` раньше сливал FIFO-очередь вывода пула **целиком** на каждом вызове +— цикл до исчерпания `free_balance` — и вызывается из шести мест, четыре внутри эвалюаторов (депозит, +вывод, leverage close, leverage convert) плюс два пути возврата капитала в кроне. Пола на строку +очереди нет: `pm_lazy_withdraw` создаёт **новый** request-объект на каждый частичный вывод, пока +`owed > 0` (одного raw достаточно), а строки одного аккаунта никогда не сливаются. Асимметрия в том, +что очередь наполняется по одной транзакции на строку, а сливает её одна невиновная транзакция позже +— при 150 k VIZ free balance тестнета один вызов мог выплатить до 150 миллионов строк. + +Фикс (владелец q#678=A): слив теперь бюджетирован. `service_lazy_withdraw_queue(db, row_limit)` +возвращает, сколько строк обработал; per-transaction call-sites передают `1` (плати только FIFO-голову +— массу подбирает крон), а новая секция крона 9 сливает остаток до общего per-block +`pm_settle_rows_per_block`-бюджета, пока `pending_withdrawals > 0`. Эта секция — liveness backstop: +очередь продолжает продвигаться до `row_budget` строк за блок, даже когда капитал не возвращается в +`free_balance`, так что она не может застрять. Строки, которые платит крон, всё равно честно чарджят +общий бюджет (возвращаемое значение вычитается из `row_budget`). + +`lazy_withdraw_queue_row_budget_spans_blocks` воспроизводит старое поведение в миниатюре — 250 +one-raw строк против 100-строкового бюджета — и ассертит, что очередь растягивается на несколько +блоков, ≤ `row_budget` за блок, FIFO-порядок, `free_balance ≥ 0` и `pending_withdrawals → 0`. С +до-фиксовым unbounded-сливом контроль сливает все 250 в первом блоке и падает на per-block границе. +Без изменения layout → тестнету передеплой не нужен. + +### 4.9 Каскад ликвидаций работает per transaction (найдено 2026-08-20, открыто) + +Всё выше ограничивает работу **за блок**. `cascade_liquidate()` ломает эту рамку, потому что +достигается из эвалюаторов — `pm_place_bet` (обе бинарные ветки), `pm_cancel_bet` и +`pm_withdraw_liquidity` — так что его стоимость платится за *транзакцию*, а блок держит столько +транзакций, сколько в него влезло. + +Сам скан неизбежен по форме: за каждый раунд он обходит status-0 плечевые позиции рынка и оценивает +`cancel_value()` против порога каждой, останавливаясь на первой жертве. Когда ликвидировать некого — +нормальный случай, тот, что коммент описывает как «cheap index probe» — он всё равно посещает +**каждую открытую позицию на этом рынке**, прежде чем заключить, что работы нет. `pm_min_bet` (1 VIZ) +— всё, что нужно для запуска одного такого свипа, и ничто не капает, сколько ставок может нести блок. + +Насколько велик может вырасти свипаемый набор, фиксируется экономикой пула, а не явным капом. Каждая +открытая позиция лочит как минимум `pm_min_liquidity` (100 VIZ) leverage-фонда (пол #536), фонд — +`pm_leverage_fund_percent` от free balance пула, а сам `free_balance` тает по мере выдачи займов, так +что неподвижная точка — примерно `N ≤ free / 1100` при сегодняшних 10 %. Пул, держащий ~11 M VIZ, +следовательно, поддерживает ~10 000 открытых позиций, а constraint 3 капит только размер отдельной +позиции, а не то, сколько их делит один рынок. При замеренных ~1.7 µs на посещённую строку это ~17 ms +работы, купленных одной ставкой в 1 VIZ, повторённой для каждой ставки блока. + +Стоит отметить, откуда взялся этот пол: `pm_min_liquidity` был наложен на займ фиксом аудита #536, и +его собственный коммент формулирует намерение — «ограничивает глобальное число открытых позиций до +`fund_total / pm_min_liquidity`». Это рассуждение корректно для работы, меряемой **за блок**, что и +есть каждый свип выше. Оно не переносится на скан, работающий раз за транзакцию: ограничение набора +ничего не говорит о том, сколько раз набор переобходится, и ничто не капает пере-проходы. + +Две честные оговорки. Во-первых, внешний цикл пересканирует с головы диапазона после каждой +ликвидации (`O(K·N)` для K ликвидаций), но K само-затухает: ликвидация продаёт токены позиции обратно +в кривую, что двигает цену *в сторону* оставшихся позиций той же стороны и делает их безопаснее, так +что массовые каскады — не ожидаемая форма. Per-bet `O(N)`-скан — та часть, что не зависит от того, +пошло ли что-то не так. Во-вторых, на тестнете сейчас ничто из этого недостижимо: +`pm_leverage_max_per_position_bp` (20 bp) против текущего фонда делает per-position кап (~30 VIZ) +меньше, чем пол займа 100 VIZ, так что позицию вообще не открыть — известный конфликт #536, +оставленный как есть владельцем (q#568). Развязка этого конфликта в пользу меньших займов +пропорционально расширит этот скан; два решения связаны. + +**Решение (владелец q#679=D, 2026-08-20): не чинить.** Плечо — нишевый продукт, выставленный набор +экономически капнут (граница fund/pool выше), а атакующий и так платит залог и funding за каждую +позицию, которую каскад должен сканировать. Цель #440 закрыта. + +### 4.10 Проверено и отвергнуто: свип открытых позиций (крон §2c) + +Секция, которая force-закрывает позиции после окончания беттинга, обходит **все** status-0 позиции +каждый блок через `by_lev_funding_due` и чарджит `done` только за закрытия, так что на первый взгляд +выглядит как §4.5 снова: полный скан, чьи скипнутые строки не стоят бюджета. + +Это не тот же дефект, и различие стоит проговорить, потому что это граница между двумя семействами. +В §4.5 голова диапазона набивалась рынками, которым работа **никогда** больше не понадобится, так +что idle-скан рос с оборотом без границы. Здесь каждая сканируемая строка — живое обязательство — +открытый займ, который цепь обязана в конце закрыть — и строка покидает диапазон навсегда в момент +закрытия (status 0 → 1). Рабочий набор поэтому тот же экономически капнутый `N`, что в §4.9, а не +бэклог трупов. Сделать это дешевле значило бы хранить дедлайн force-close на позиции и ключовать по +нему индекс: изменение layout и миграция снапшота ради constant-factor выигрыша на наборе, который +уже ограничен. Не стоит того; записано, чтобы следующий аудит не открывал заново. Цель #441 закрыта +на этом рассуждении. + +### 4.11 Idle fast-path батч-исполнителя (крон §6, найдено 2026-08-20, исправлено) + +§4.4 поставил батч-исполнителя, чарджящего row-бюджет за каждую *посещённую* строку, но **внешний** +проход всё ещё был `O(active batch markets)` на границу и не метрирован: idle-рынок (ничего в очереди +на текущей эпохе) выскакивал до LMSR q-vector снапшота и не платил ничего — ни `done`, ни row-бюджет +— так что per-boundary скан рос линейно с *числом рынков* независимо от того, сколько очереди реально +существовало. Спамер мог создать тысячи `allow_batch` рынков и превратить каждую границу эпохи в +полный idle-проход. + +Фикс (P0): индекс `by_status_market` на `pm_bet` — `(status, market, id)` — и скан теперь едет прямо +от queued (`status=5`) строк. Только рынки, которые реально держат очередь, вообще посещаются; idle- +рынки никогда не входят в диапазон. Resume-семантика не меняется: исполненные строки флипают +`5 → 0/2` и покидают индекс, так что следующий `lower_bound((5, market, bet))` естественно +приземляется на следующей неисполненной строке, а два курсора dynamic-properties +(`pm_batch_settle_cursor`, `pm_batch_settle_bet_cursor`) сохраняют смысл (market / parked-row). +Защитный скип перешагивает status-5 строки, которые когда-либо протекут на неактивный рынок — §2/H2 +рефанд уже платит их, а проход не должен двигать их между проходами меряния и оплаты. Цель #445 +закрыта. + +### 4.12 Purge deferred claims и loud-сигналы фаз 1/5 (2026-08-20, исправлено) + +Два меньших остатка того же ревью, взятые вместе с P0 (владелец выбрал C): + +- **P1 — `purge_deferred_claims` строил in-memory вектор** каждого deferred claim перед удалением, + тот же participant-vector паттерн, что нёс `refund_all_bets` (и который §4.3 убрал). Claims уже + капнуты `MAX_PM_DEFERRED_CLAIMS_PER_MARKET` (10 000), так что это никогда не было проблемой границы + — но вектор бесплатно сбрасывается: теперь он удаляет на месте, продвигая итератор до того, как + `db.remove` его инвалидирует. Цель #446 закрыта. + +- **P2 — неметрированные фазы сеттлмента 1 и 5.** `settle_market_step` бюджетирует только фазы 2–4 + (bet-строки). Фаза 1 (`force_close_positions`) и фаза 5 (`settle_liquidity`) обходят каждую открытую + позицию / LP-строку рынка целиком; они ограничены только экономически (каждая строка ≥ + `pm_min_liquidity` = 100 VIZ). Вместо метрирования их — курсоры и resume ради угрозы, которую пол + 100 VIZ уже выценивает — код теперь эмитит **loud log-only сигнал** (зеркаля сигнал F1 `uncovered`), + когда один из этих проходов перешагивает `pm_settle_rows_per_block` за один шаг. Он детерминирован + и не имеет консенсус-эффекта; существует, чтобы будущее изменение, снижающее залоговый пол + (например, развязка #536 в сторону мелких займов), сделало фазу явно дорогой вместо молчаливой + деградации блок-тайма. Цель #448 закрыта. + +## 5. Что реально стоит строка + +Замерено `tests/consensus_sim/bench/settle_bench.cpp` (`make pm_settle_bench`), который гонит реальные +рынки растущего числа строк через реальную цепь и таймит блок, который их сеттлит. Release-сборка, +без санитайзеров, без плагина account_history: + +| строки | idle блок | сеттлящий блок | на строку | gc блок | gc на строку | +|---|---|---|---|---|---| +| 500 | 0.28 ms | 1.24 ms | 1.93 µs | 0.59 ms | 0.62 µs | +| 2 000 | 0.29 ms | 3.61 ms | 1.66 µs | 1.54 ms | 0.62 µs | +| 8 000 | 0.29 ms | 13.95 ms | 1.71 µs | 5.50 ms | 0.65 µs | + +Сеттлмент линеен по строкам на **~1.7 µs/строку**, garbage collection на ~0.62 µs/строку. +Экстраполируя: примерно **600 000 строк заполняют одну секунду** блок-тайма и ~1.7 M строк заполняют +весь трёхсекундный интервал. Считай это **нижней** границей — бенчмарк ставит с десяти аккаунтов +(у реального рынка их тысячи, так что лукапы менее cache-friendly), а симулированная нода не гонит +плагин account_history, так что virtual op, пушимая на строку, там стоит почти ничего, тогда как +API-нода платит за её индексацию. + +Такова форма риска при одном фиксе A: органический рынок нигде рядом с лимитом (тестнет-рынок в 734 +строки сеттлится за ~1.2 ms), но спамер, готовый стейкать 1 VIZ на строку, может купить ~1.7 секунды +сеттлмент-работы в одном блоке примерно за миллион VIZ. Достаточно дёшево, чтобы стоило закрыть — это +и есть фикс D. + +## 6. Тюнинг + +`pm_settle_rows_per_block` меняет латентность сеттлмента на блок-тайм, и замер выше — то, от чего его +следует задавать: + +* default **2 000** стоит ~3.4 ms сеттлмент-работы на блок (около 0.1 % интервала) и сливает рынок в + миллион строк за ~500 блоков, т.е. менее чем за полчаса; +* поднять до 10 000 стоит ~17 ms/блок и сливает тот же рынок за ~100 блоков; +* пол 100 существует, чтобы прогресс был гарантирован всегда. + +Поднять `pm_min_bet` вместо этого укорачивает очередь ценой исключения мелких бетторов — предпочти +сначала тюнинг бюджета. diff --git a/@l10n/ru/docs/prediction-markets/specification.md b/@l10n/ru/docs/prediction-markets/specification.md new file mode 100644 index 0000000000..191386973b --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/specification.md @@ -0,0 +1,1013 @@ +--- +title: Onix Protocol — Спецификация +description: Формальная техническая спецификация протокола Onix, реализованного как операции консенсуса на VIZ DLT (HF14). +--- + +# Спецификация Onix Protocol + +**Версия:** 2.0 (on-chain / HF14) +**Статус:** Формальная техническая спецификация — реализована как операции консенсуса на VIZ DLT + +--- + +> **On-chain (HF14).** Реализовано как операции консенсуса первого класса (`pm_*`) на VIZ DLT и проверено +> в `consensus_sim`. **Все проценты — базисные пункты (bp): 10000 = 100.00%**; все длительности — +> governance-параметры в **секундах / блоках**. Медиана-голосуемые параметры живут в структуре +> `chain_properties_pm` (§3); per-market поля — операция `pm_create_market`; всё состояние — в +> chainbase-объектах §17. Комиссии оракула — offer→quote (потолок создателя → оракул фиксирует котировку +> при акцепте, эмитя `pm_market_accepted`). У споров два режима — комитет (`dispute_mode = 0`, по +> умолчанию: stake-weighted **публичный** `pm_dispute_vote`, изменяем до закрытия, стейк Lazy-Pool +> считается) и аккаунт (`dispute_mode = 1`: именованный `dispute_resolver`). + +## Оглавление + +1. Определения и роли +2. Валюта и точность +3. Системные параметры +4. Машина состояний рынка +5. Onix Binary: Constant Product Market Maker +6. Onix Multi: LMSR с parimutuel-расчётом +7. Структура комиссий +8. Time penalty для поздних ставок +9. Предоставление ликвидности +10. Резолюция и выплаты +11. Отмена ставки +12. Система споров +13. Штраф оракула за пропуск резолюции +14. Скоринг репутации оракула +15. Трансфер позиций +16. Lazy-пул ликвидности +16a. Опциональное плечо +16b. Batch / Commit-Reveal ставки +17. On-chain модель объектов + +--- + +## 1. Определения и роли + +| Роль | Определение | +|------|-----------| +| **Создатель рынка** | Платит `pm_market_creation_fee` (`pm_create_market`); задаёт вопрос, исходы, ликвидность, потолки комиссий и параметры тайминга | +| **Оракул** | Регистрируется (комиссия: `pm_oracle_registration_fee`), вносит страховку (мин.: `pm_min_oracle_insurance`), котирует свои условия комиссии в **базисных пунктах** (≤ потолка создателя) + фиксированную комиссию при акцепте, принимает/отклоняет рынки, даёт решения по исходам | +| **Беттер** | Ставит на исходы; получает токены пропорционально стейку и текущим резервам | +| **Поставщик ликвидности (LP)** | Поставляет капитал в пулы рынка; зарабатывает time-weighted долю комиссий ликвидности + штрафной пул | +| **Поставщик Lazy Pool** | Вносит VIZ в Lazy-пул ликвидности с lock-периодом; пул авто-аллоцирует в рынки и распределяет награды через аккумулятор `reward_per_share` | +| **Резолвер споров** | Только режим аккаунта (`dispute_mode = 1`): per-market аккаунт `dispute_resolver` арбитрирует. Режим комитета (`dispute_mode = 0`) резолвера не использует — голосует электорат SHARES | +| **Фонд DAO / комитета** | Существующий фонд комитета цепи. Получает `pm_market_creation_fee` и доп. штрафы оракулов | + +--- + +## 2. Валюта и точность + +Все суммы хранятся как целые с точностью = 1/1000 (милли-VIZ). `1000` внутренних единиц = 1.000 VIZ. + +Значения time penalty используют точность = 1/1 000 000 (микро-единицы). + +--- + +## 3. Системные параметры + +### Медиана-голосуемые параметры (`chain_properties_pm`) + +Все экономические параметры голосуются медианой делегатов (без хардфорка для настройки) и живут в on-chain +структуре `chain_properties_pm`. Каждый делегат публикует свои предпочтительные значения через стандартную +**`versioned_chain_properties_update_operation`** (op ID 46) — `chain_properties_pm` является текущей +(v5, HF14) версией этой versioned-структуры — а сеть применяет **медиану по каждому полю** активных +делегатов. Две ручки риск-покрытия (`pm_listing_min_coverage_percent`, `pm_betting_min_coverage_percent`) +входят в ту же v5-структуру и настраиваются ровно так же. **Все проценты — базисные пункты +(bp, 10000 = 100.00%); длительности — в секундах или блоках** — за исключением двух ручек покрытия, которые +измеряются в проценте от объёма (100 = 1.0×). Точные дефолты и диапазоны — в +[Chain Properties](../governance/chain-properties#pm-parameters); авторитетный источник — сама структура. + +| Группа | Параметры | +|---|---| +| Регистрация и полы | `pm_oracle_registration_fee`, `pm_min_oracle_insurance`, `pm_market_creation_fee`, `pm_min_liquidity`, `pm_max_outcomes`, `pm_max_market_duration` | +| Комиссии и штрафы (bp) | `pm_max_oracle_fee_percent`, `pm_oracle_penalty_percent`, `pm_no_contest_penalty_percent`, `pm_default_time_penalty_percent`, `pm_max_time_penalty` | +| Окно акцепта | `pm_oracle_accept_window_sec` (по умолчанию 3600 = 1 ч; пендинг-рынки, не принятые/отклонённые в этот срок, аннулируются кроном — сид возвращён, комиссия за создание удержана) | +| Риск / покрытие (% от объёма) | `pm_listing_min_coverage_percent` (250 = 2.5×; рынки с покрытием ниже этого скрыты из каталога по умолчанию, показываются через `show_risky`), `pm_betting_min_coverage_percent` (150 = 1.5×; рекомендательный клиентский порог подтверждения риска, `≤` листингового, on-chain не навязывается) | +| Споры | `pm_dispute_fee`, `pm_dispute_grace_sec`, `pm_oracle_dispute_response_sec`, `pm_dispute_vote_period_sec`, `pm_dispute_auto_close_sec`, `pm_dispute_approve_min_percent` (bp), `pm_dispute_reward_multiplier` (bp) | +| Lazy-пул | `pm_lazy_pool_enabled`, `pm_lazy_alloc_percent`, `pm_lazy_max_total_alloc_percent`, `pm_lazy_recall_step_percent`, `pm_lazy_lock_sec`, `pm_lazy_emergency_penalty_percent`, `pm_lazy_min_liquidity_fee_percent` (по умолчанию 200 = 2%; пул пропускает рынки, чей `liquidity_fee_percent` ниже этого порога вознаграждения) | +| Плечо | `pm_leverage_enabled`, `pm_leverage_fund_percent`, `pm_leverage_max_per_position_bp`, `pm_leverage_max_position_ratio_percent`, `pm_leverage_min_market_liquidity`, `pm_leverage_safety_margin_percent`, `pm_leverage_max_slippage_percent`, `pm_leverage_m_factor_percent`, `pm_leverage_pool_profit_percent`, `pm_leverage_expiration_buffer_sec`, `pm_conversion_profit_cost_percent` | +| Batch / commit-reveal | `pm_commit_reveal_enabled`, `pm_batch_epoch_blocks`, `pm_reveal_window_blocks`, `pm_commit_no_reveal_penalty_percent` (bp), `pm_min_batch_bet` | +| Обработка | `pm_processing_cap_per_block` | + +Получатель `pm_market_creation_fee` и доп. штрафов оракулов — существующий фонд комитета/DAO цепи, а не +отдельный PM-аккаунт. + +### Per-market параметры (операция `pm_create_market`) + +Задаются создателем при создании; поля комиссии оракула — это **потолок**, против которого оракул котирует +при акцепте (offer→quote). Полный референс полей: [Операции прогнозных рынков](../protocol/operations/prediction-markets). + +| Поле | Описание | +|---|---| +| `oracle`, `market_type` (0 binary / 1 multi), `outcomes`, `url` | определение рынка | +| `oracle_fee_percent`, `oracle_fixed_fee` | **потолок** комиссии оракула (bp + фикс.); оракул фиксирует котировку ≤ него (и ≤ медианного `pm_max_oracle_fee_percent`) при акцепте | +| `creator_fee_percent`, `liquidity_fee_percent` | комиссии создателя и LP (bp от пула проигравших) | +| `liquidity`, `lmsr_b` | сид-ликвидность; `lmsr_b` для мульти-рынков | +| `betting_expiration`, `result_expiration` | таймеры | +| `time_penalty_type`, `time_penalty_value`, `penalty_curve_type` | форма штрафа за поздние ставки | +| `allow_early_resolution`, `allow_cancellation` | переключатели | +| `allow_batch`, `allow_instant_bet` | режимы ставок (бинарные) | +| `endogeneity_tier` | 1 эконом-данные / 2 спорт / 3 политика (подсказка отображения/риска) | +| `dispute_mode` (0 комитет / 1 аккаунт), `dispute_resolver` | маршрутизация споров | +| `dispute_penalty_percent` | политика штрафа оракула при удовлетворённом споре (bp, со знаком) | +| `metadata` | свободный клиентский JSON (консенсус-непрозрачен; парсится off-chain) | + +--- + +## 4. Машина состояний рынка + +### Состояния + +| Status | Имя | Описание | +|--------|------|-------------| +| -1 | Deleted | Оракул отклонил **или** истекло окно акцепта (`pm_oracle_accept_window_sec`); сид-ликвидность возвращена создателю (комиссия за создание удержана) | +| 0 | Waiting | Ожидает проверки оракула | +| 1 | Active | Принимает ставки до `betting_expiration` | +| 2 | Closed | Приём ставок закончен, ожидает резолюции оракула | +| 3 | Resolved | Исход определён, выплаты рассчитаны | + +### Состояния выплат + +| payout_status | Имя | Описание | +|---------------|------|-------------| +| 0 | Not calculated | До резолюции | +| 1 | Calculated | Выплаты ожидают (grace-период активен) | +| 2 | Paid | Все выплаты обработаны | +| 3 | Disputed | Подан спор, выплаты заморожены | + +### Переходы + +```mermaid +stateDiagram-v2 + direction LR + state "Waiting (0)" as Waiting + state "Active (1)" as Active + state "Closed (2)" as Closed + state "Resolved (3)" as Resolved + state "Deleted (-1)" as Deleted + state "Paid out" as Paid + [*] --> Waiting + Waiting --> Active: оракул принял + Waiting --> Deleted: оракул отклонил + Waiting --> Deleted: окно акцепта истекло (pm_market_expired) + Active --> Closed: betting_expiration + Active --> Resolved: раннее разрешение (если разрешено) + Closed --> Resolved: оракул разрешает + Resolved --> Paid: окно ожидания (12ч), без спора + Deleted --> [*] + Paid --> [*] +``` + +**Предусловия:** + +| Переход | Предусловия | +|-----------|---------------| +| 0 → 1 | Страховка оракула ≥ `min_oracle_insurance`; оракул принимает | +| 0 → 1 (self-oracle) | Создатель = оракул; проверка страховки; авто-одобрение при создании | +| 0 → -1 | Оракул отклоняет; сид-ликвидность возвращена создателю | +| 0 → -1 (экспирация) | `now ≥ created_time + pm_oracle_accept_window_sec` без действия оракула; крон аннулирует рынок, возвращает сид (комиссия за создание удержана), эмитит `pm_market_expired` | +| 1 → 3 | Оракул подаёт резолюцию с исходом (0, 1 или -1 для no-contest); `allow_early_resolution=1` или `time ≥ betting_expiration` | +| 2 → 3 | Оракул подаёт резолюцию; `time ≤ result_expiration` | +| 3 → paid | Grace-период прошёл без спора; крон обрабатывает выплаты | + +### Флоу создания рынка + +1. Списать `market_creation_fee` с создателя → фонд DAO (невозвратно) +2. Записать `oracle_fixed_fee` из профиля оракула на рынок +3. Заблокировать `liquidity` с баланса создателя +4. Инициализировать резервы: `reserve_a = floor(liquidity/2)`, `reserve_b = liquidity − reserve_a` +5. Вычислить `k = reserve_a × reserve_b` +6. Если self-oracle: авто-одобрение до status=1 с проверкой страховки +7. Если внешний оракул: войти в status=0 и установить `accept_deadline = created_time + pm_oracle_accept_window_sec` + +### Флоу акцепта оракула + +Пендинг-рынок должен быть разрешён своим оракулом в течение окна акцепта +(`pm_oracle_accept_window_sec`, по умолчанию 1 ч). Три исхода: + +- **Принятие** (status 0 → 1): (1) перевести `oracle_fixed_fee` с баланса создателя на баланс оракула + (пропускается для self-oracle); (2) инкремент `markets_accepted`; (3) обновить `last_active_time`; + (4) запуск авто-аллокации Lazy Pool (если у пула есть свободный баланс **и** `liquidity_fee_percent + рынка ≥ pm_lazy_min_liquidity_fee_percent`). +- **Отклонение** (status 0 → -1): сид-ликвидность возвращена создателю; без vop. +- **Экспирация** (status 0 → -1): если ничего не произошло к `accept_deadline`, крон каждого блока + аннулирует рынок, возвращает сид-ликвидность (**не** комиссию за создание) и эмитит `pm_market_expired`. + +### Audit Trail + +Каждое state-changing действие — это операция консенсуса или виртуальная операция, навсегда записываемая в +block log и запрашиваемая через `account_history`. Ставки, отмены, добавление/вывод ликвидности, +accept/reject, резолюция, спор, разрешение спора, выплата и штраф появляются как `pm_*`-операции/vop, +вместе с резервами рынка, которых они касаются. + +--- + +## 5. Onix Binary: Constant Product Market Maker + +### Инвариант + +``` +k = reserve_a × reserve_b +``` + +`k` меняется только при операциях добавления/вывода ликвидности. + +### Размещение ставки (сторона A) + +``` +new_reserve_b = reserve_b + amount +new_reserve_a = floor(k / new_reserve_b) +tokens_received = reserve_a − new_reserve_a +price = amount × 1,000,000 / tokens_received +``` + +Симметрично для стороны B (поменять a/b). + +### Защита от слиппеджа + +Опциональный параметр `min_tokens` в `place-bet`. Если `tokens_received < min_tokens`, транзакция +отвергается. + +### Инициализация рынка + +``` +reserve_a = floor(liquidity / 2) +reserve_b = liquidity − reserve_a +k = reserve_a × reserve_b +``` + +Минимальная начальная ликвидность: 100,000 mVIZ (100 VIZ). + +### Семантика веса (токена) + +- `weight` = число токенов исхода, полученных беттером (задаётся CPMM на момент ставки) +- `weight` — это **относительная претензия**, не выплата в VIZ. Расчёт **parimutuel** (идентично Onix + Multi): победители получают назад стейк плюс пропорциональную долю пула проигравших, по весу. +- Если ставка на сторону A и выигрывает исход A: `payout = bet_amount + (weight / total_winning_weight) × winners_pool − time_penalty_on_profit` +- Если исход A проигрывает: payout = 0 (стейк форфейтится в пул победителей) + +CPMM — это **движок ценообразования** (вероятность + назначение веса); он больше не гейтит выплату. Это +заставляет два типа рынков разделять одну модель расчёта: *AMM назначает веса (CPMM для бинарных, LMSR +для мульти); проигравшие финансируют победителей пропорционально весу.* + +### Отображение цены + +``` +implied_probability_A = reserve_b / (reserve_a + reserve_b) × 100% +implied_probability_B = reserve_a / (reserve_a + reserve_b) × 100% +``` + +### Гарантия принципала LP (доказательство) + +При parimutuel-расчёте гарантия точна и не опирается на геометрию кривой: + +``` +Money OUT = L (LP principal) + Σ(winning bet_amount) + winners_pool + fees + = L + winning_bets + (losers_sum − fees) + fees + = L + winning_bets + losing_bets = L + all_bets = Money IN +``` + +Суммарная выплата ограничена `losers_sum` независимо от весов, поэтому принципал LP `L` возвращается +безусловно, а победители фондируются целиком проигравшими. (Легаси AM-GM граница +`reserve_a + reserve_b ≥ L` больше не нужна для платёжеспособности; она остаётся свойством ценовой +кривой.) + +--- + +## 6. Onix Multi: LMSR с parimutuel-расчётом + +### Функция цены (softmax) + +Для N исходов с параметрами количества q_1, ..., q_N и параметром ликвидности b: + +``` +price(i) = exp(q_i / b) / Σ_j exp(q_j / b) +``` + +**Инвариант:** `Σ_i price(i) = 1` (по определению softmax). + +### Функция стоимости + +``` +C(q) = b × ln(Σ_j exp(q_j / b)) +``` + +Стоимость покупки Δ токенов на исход i: + +``` +cost = C(q + Δ·e_i) − C(q) + = b × [ln(Σ_j exp(q'_j / b)) − ln(Σ_j exp(q_j / b))] +where q'_i = q_i + Δ, all other q'_j = q_j +``` + +Численная устойчивость (трюк log-sum-exp): + +``` +ln(Σ exp(x_j)) = max(x) + ln(Σ exp(x_j − max(x))) +``` + +### Параметр ликвидности + +``` +b = S / ln(N) +``` + +где S = депозит субсидии LP, N = число исходов. + +### Расчёт (на резолюции) + +``` +1. Oracle declares winning outcome +2. losers_sum = Σ bet_amount for all non-winning bets +3. oracle_fee = floor(losers_sum × oracle_fee_percent / 10000) +4. creator_fee = floor(losers_sum × creator_fee_percent / 10000) +5. liq_fee = floor(losers_sum × liquidity_fee_percent / 10000) +6. winners_pool = losers_sum − oracle_fee − creator_fee − liq_fee +7. For each winning bettor: + payout = bet_amount + (their_tokens / total_winning_tokens × winners_pool) − time_penalty +8. LP subsidy returned unconditionally +9. LP earns time-weighted share of liq_fee +``` + +### Гарантия принципала LP (доказательство) + +1. LP вносит S VIZ как субсидию. Это задаёт b = S / ln(N). +2. Во время ставок пользователи платят VIZ → получают токены. VIZ накапливается как пул ставок. +3. На резолюции: проигравшие форфейтят 100% → `losers_sum`. Победителям платят из `losers_sum` (не из + субсидии). +4. Субсидия LP S возвращается **безусловно** — она архитектурно отделена от потока выплат. + +### Граничные случаи + +| Сценарий | Исход | +|----------|---------| +| Все ставки на выигрышный исход | `losers_sum=0`, `winners_pool=0`. Каждый беттер получает назад `bet_amount`. Субсидия LP возвращена. | +| Нет ставок на выигрышный исход | `losers_sum=total_bets`. Нераспределённый `winners_pool` → бонус LP. | +| Рынок с нулевым объёмом | Субсидия LP возвращена. Ни комиссий, ни выплат. | +| Выигрывает единственный беттер | Беттер получает `bet_amount + winners_pool`. Субсидия LP возвращена. | + +### Операции + +| Операция | Описание | +|-----------|-------------| +| `pm_create_market_multi { oracle, outcomes, liquidity, fees, ... }` | Создать рынок с N исходами | +| `pm_place_bet_multi { market, outcome_index, amount, min_tokens }` | Купить токены исхода | +| `pm_cancel_bet_multi { bet_id, min_return }` | Продать токены назад через reverse LMSR | +| `pm_add_liquidity_multi { market, amount }` | Добавить субсидию LP (увеличивает b) | +| `pm_withdraw_liquidity_multi { liquidity_id }` | Вывести субсидию LP (мин. floor навязан) | +| `pm_resolve_multi { market, winning_outcome }` | Оракул объявляет победителя, запускает расчёт | + +Бинарные рынки (N=2) используют Onix Binary (CPMM). LMSR используется только для N > 2. + +--- + +## 7. Структура комиссий + +### Вычисление комиссий на момент резолюции + +Все процентные комиссии считаются на резолюции от **общего объёма проигравшей стороны**: + +``` +losers_sum = Σ bet_amount for all losing bets + +oracle_fee = floor(losers_sum × oracle_fee_percent / 10000) +creator_fee = floor(losers_sum × creator_fee_percent / 10000) +liquidity_fee = floor(losers_sum × liquidity_fee_percent / 10000) +winners_pool = losers_sum − oracle_fee − creator_fee − liquidity_fee +``` + +Комиссии НЕ удерживаются со ставок при размещении. Полная сумма ставки входит в резервы CPMM/LMSR. + +### Фиксированная комиссия оракула + +Разовая комиссия на рынок. Задаётся оракулом в профиле. Платится создателем оракулу при акцепте рынка. +Полностью пропускается для self-oracle рынков (балансовой операции не происходит). + +### Поля учёта комиссий + +- `oracle_fee_earned` — не используется на резолюции; комиссия считается из losers_sum +- `liquidity_fee_earned` — фактически выплаченная LP-комиссия рынка (счётчик, пишется в `settle_liquidity` пост-фактум; до сеттла и на void-рынках равен 0). Ранний выход LP комиссии не приносит: `pm_withdraw_liquidity` требует `status == 0`, а `earned_fee` присваивается вместе с `status = 3` при сеттле, так что вывод этот доход увидеть не может. При сеттле бонус LP = `floor(losers_sum × liquidity_fee_percent / 10000) + penalty_pool + нераспределённая пыль` (§7), делится между активными LP пропорционально amount·time. +- Per-bet `oracle_fee` и `liquidity_fee` записываются для аудита; не аккумулируются на рынке + +### Округление + +Все вычисления используют `floor()`. Нераспределённый dust (< 1 mVIZ) отправляется в фонд DAO при +финальной выплате. + +--- + +## 8. Time penalty для поздних ставок + +### Окно штрафа + +| Тип | Вычисление окна | +|------|-------------------| +| Fixed (type=0) | `penalty_window = time_penalty_value` секунд до экспирации | +| Percentage (type=1) | `penalty_window = time_penalty_value / 100 × (betting_expiration − market_creation_time)` | + +### Вычисление штрафа + +``` +time_to_expiration = betting_expiration − current_time + +if time_to_expiration < penalty_window: + ratio = 1 − (time_to_expiration / penalty_window) + + if penalty_curve_type == 1: // quadratic + penalty_ratio = ratio × ratio + else: // linear + penalty_ratio = ratio + + time_penalty = floor(penalty_ratio × max_time_penalty) +else: + time_penalty = 0 +``` + +### Применение при выплате (только к прибыли) + +``` +profit = floor(winners_pool × weight / total_winning_weight) // parimutuel share of losers' pool +penalty_deduction = floor(profit × time_penalty / 1,000,000) +net_payout = bet_amount + profit − penalty_deduction +``` + +**Инвариант:** `net_payout ≥ bet_amount` — штраф применяется только к доле прибыли, поэтому победители +всегда получают не меньше принципала. (Идентично для Onix Binary и Onix Multi.) + +--- + +## 9. Предоставление ликвидности + +### Добавление ликвидности + +``` +add_a = amount × reserve_a / (reserve_a + reserve_b) +add_b = amount − add_a +new_reserve_a = reserve_a + add_a +new_reserve_b = reserve_b + add_b +new_k = new_reserve_a × new_reserve_b +``` + +Записывает `sec_to_expiration = betting_expiration − current_time` на момент депозита. + +### Time-weighted распределение комиссий (на резолюции) + +``` +fee_pool = remaining_liquidity_fee + total_penalty_pool + +weight_i = amount_i × max(1, sec_to_expiration_i) +total_weight = Σ weight_i +fee_share_i = floor(fee_pool × weight_i / total_weight) +lp_payout_i = principal_i + fee_share_i +``` + +Каждый депозит — независимая позиция. Несколько депозитов одного пользователя отслеживаются раздельно. + +### Ранний вывод + +**Предусловия:** market status=1, `time < betting_expiration`, `resulting liquidity_sum ≥ 100,000 mVIZ`. + +``` +// Fractional withdrawal +fraction = withdraw_amount / lp_amount +withdraw_weight_a = floor(weight_a × fraction) +withdraw_weight_b = floor(weight_b × fraction) + +// Reverse reserves +new_reserve_a = reserve_a − withdraw_weight_a +new_reserve_b = reserve_b − withdraw_weight_b +new_k = new_reserve_a × new_reserve_b + +// Time-ratio discount +time_served = current_time − lp_deposit_time +market_duration = betting_expiration − market_creation_time +time_ratio = min(1, time_served / market_duration) + +// Fee share (conservative: min of both sides) +fee_from_a = floor(a_bets_sum × liquidity_fee_percent / 10000) +fee_from_b = floor(b_bets_sum × liquidity_fee_percent / 10000) +estimated_pool = min(fee_from_a, fee_from_b) − already_paid_to_early_lps +lp_tw = withdraw_amount × max(1, sec_to_expiration) +total_tw = Σ (active LP time-weights) +raw_fee_share = floor(estimated_pool × lp_tw / total_tw) +fee_share = floor(raw_fee_share × time_ratio) + +returned = withdraw_amount + fee_share +``` + +**Post-expiration lock:** Вывод LP блокируется при `time ≥ betting_expiration`. Все позиции LP заблокированы +до резолюции. + +### Безопасность принципала при раннем выводе + +Вывод вычитает исходные `weight_a` и `weight_b` (не пропорциональную долю текущих резервов). Если +`reserve_a < weight_a` или `reserve_b < weight_b`, вывод **блокируется**. + +### Создатель как первый LP + +Создатель рынка автоматически первый LP. Его `sec_to_expiration` равен полной длительности рынка, давая +максимальный time-weight. + +--- + +## 10. Резолюция и выплаты + +### Приоритет выплат + +| Приоритет | Тип | Получатель | Сумма | +|----------|------|-----------|--------| +| 1 | Oracle fee (2) | Оракул | `floor(losers_sum × oracle_fee_percent / 10000)` | +| 1.5 | Creator fee (7) | Создатель | `floor(losers_sum × creator_fee_percent / 10000)` | +| 2 | Creator LP (1) | Создатель | `principal + time-weighted fee share` | +| 3 | LP return (1) | LP | `principal + time-weighted fee share` | +| 4 | Winner bets (0) | Победители | `bet_amount + floor(winners_pool × weight / total_winning_weight) − penalty_deduction` (parimutuel) | +| 5 | Dispute refund (5) | Участники спора | (если применимо) | +| 6 | Oracle penalty bonus (6) | Все участники | (если оракул оштрафован) | + +### Проигравшая сторона + +Выплата = 0. Стейки поглощены в резервный пул. + +### Рынки с нулевым объёмом + +LP получают полный принципал. Оракул получает фикс. комиссию (если есть). Все аккумуляторы комиссий +остаются 0. + +--- + +## 11. Отмена ставки + +### Предусловия + +| Условие | Проверка | +|-----------|-------| +| Ставка активна | `bet.status == 0` | +| Пользователь владеет ставкой | `bet.user == current_user.id` | +| Рынок активен | `market.status == 1` | +| Приём открыт | `current_time < market.betting_expiration` | +| Отмена разрешена | `market.allow_cancellation == 1` | + +### Механика reverse CPMM + +Для ставки на сторону A (side=0): + +``` +new_reserve_a = reserve_a + tokens +new_reserve_b = floor(k / new_reserve_a) +amount_returned = reserve_b − new_reserve_b +if amount_returned <= 0: amount_returned = 0 +``` + +Симметрично для стороны B. + +### Защита от слиппеджа + +Опциональный параметр `min_return`. Если `amount_returned < min_return`, транзакция отвергается. + +### Изменения состояния (атомарно) + +1. Статус ставки → 1 (отменена), записан `returned_amount` +2. Резервы рынка обновлены +3. Суммы ставок рынка уменьшены на исходную сумму ставки +4. Баланс пользователя увеличен на `amount_returned`, `bets_balance` уменьшен +5. Запись истории (type=4) +6. Запись market log с резервами до/после + +--- + +## 12. Система споров + +### Предусловия подачи + +- Подающий разместил ставку на рынке +- В течение `pm_dispute_grace_sec` после резолюции +- Маршрутизация по режиму: **комитет** (`dispute_mode = 0`) — аккаунт-резолвер не нужен, электорат SHARES голосует через `pm_dispute_vote` (публично, изменяемо до `voting_end_time`, вес = `effective_vesting_shares` + стейк Lazy-Pool→shares), подсчёт кроном `pm_dispute_finalize`; **аккаунт** (`dispute_mode = 1`) — именованный `dispute_resolver` рынка выносит `pm_dispute_resolve` +- Нет открытого спора на рынке +- Подающий платит `pm_dispute_fee` + +### Ответ оракула + +Обязателен в течение `pm_oracle_dispute_response_sec`. При пропуске `pm_dispute_fee` авто-слешится из +страховки и записывается на объект оракула. + +Оракул публикует свой контраргумент через **`pm_dispute_oracle_respond`** (op ID 98). Поскольку спор — это +открытое публичное слушание, текст хранится **на объекте спора** (`oracle_response` + `oracle_response_time`, +читается через `get_dispute`), чтобы каждый голосующий комитета или аккаунт-резолвер мог взвесить его перед +решением. Отвечать может только оракул рынка, только пока спор открыт и `now ≤ oracle_response_deadline`; +повторная публикация перезаписывает предыдущий ответ. + +### Жизненный цикл спора + +``` +Resolution (T=0) → Grace period (T to T+12h) → Dispute filed (T≤12h) + → Oracle response (12h window) → Resolver decision (up to 14 days) + → After verdict: recalculate or unfreeze → Auto-payout after new grace period + → Auto-close fallback (T+14 days): full refund + oracle penalty +``` + +### Спор удовлетворён (оракул неправ — переворот) + +Награда диспутёру — carve-out из слэша; **остаток слэша финансирует победивших бетторов** (через +`forfeit_pool`), не резолвера и не ДАО. **Ни голосующие комитета, ни аккаунт-резолвер не оплачиваются.** + +``` +reward_target = floor(dispute_fee × pm_dispute_reward_multiplier / 10000) // bp; 30000 = ×3 +bonus = max(0, reward_target − dispute_fee), ограничен слэшем + +1. Disputer ← dispute_fee (escrow назад) + bonus // bonus берётся из слешнутой страховки +2. forfeit_pool += (slash − bonus) // → победителям, через winners_pool на сеттлменте +``` + +Размер слэша: режим комитета масштабирует `dispute_penalty_percent` оракула на `consensus_strength`; +режим аккаунта берёт `penalty_amount` резолвера (оба ограничены остатком страховки). + +### Спор отклонён (оракул прав — подтверждён) + +``` +Disputer отдаёт всю dispute_fee → оракулу (100%, компенсация). // без 50/50 резолвер/ДАО +``` + +### Процесс пересчёта (оракул неправ) + +1. Валидировать штраф (ограничен оставшейся страховкой) +2. Выплачена награда диспутёру (fee + bonus); остаток слэша → `forfeit_pool` (победителям) +3. Слешинг страховки оракула +5. Применены баны (если запрошены) +6. Удалить все существующие невыплаченные payouts +7. Перевернуть выигрышный исход (A↔B) +8. Заново сгенерировать выплаты с исправленным исходом +9. Записан audit trail + +### Полномочия резолвера — баны это compliance/регуляторная функция (только режим аккаунта) + +Санкции ниже — это поля **аккаунт-режимной** `pm_dispute_resolve` (op ID 80), выносимой именованным +`dispute_resolver` рынка. Когда этот резолвер — **регулятор или лицензированный арбитр**, именно так он +принуждает к off-chain-правилам: одним вердиктом он может слешить страховку **и забанить как оракула, так и +создателя рынка** на платформе, временно или навсегда. **Режим комитета/DAO (`dispute_mode = 0`) по замыслу +не имеет полномочий бана** — публичное слушание только слешит страховку (масштабируемую силой консенсуса) и +корректирует репутацию через `pm_dispute_finalize`; оно никогда не банит. + +| Параметр | Тип | Описание | +|-----------|------|-------------| +| `penalty_amount` | mVIZ | Дополнительный слеш страховки (0 до остатка) → фонд DAO | +| `ban_oracle` | 0/1 | Забанить оракула | +| `ban_oracle_until` | unix ts / 0 | 0=навсегда, >0=истекает | +| `ban_creator` | 0/1 | Забанить создателя | +| `ban_creator_until` | unix ts / 0 | 0=навсегда, >0=истекает | + +Бан записывает выносящего `resolver` в поле `banned_by` цели. **Снятие бана:** тот же резолвер может снять +его **досрочно** через **`pm_unban`** (op ID 99, `unban_oracle` / `unban_creator`); иначе он просто истекает +на `banned_until`, после чего per-block крон очищает его и эмитит виртуальную операцию **`pm_ban_expired`** +(ID 100), чтобы история/индексаторы наблюдали снятие. + +### Авто-закрытие (14-дневный fallback) + +| Действие | Описание | +|--------|-------------| +| Истец | Dispute fee возвращена | +| Оракул | `dispute_fee` слешится из страховки | +| Ставки | Все возвращены (исходные суммы) | +| LP | Все возвращены (только принципал) | +| Распределение штрафа | Слешнутая сумма распределена пропорционально всем участникам | +| Статус спора | Установлен в 3 (авто-закрыт) | + +### Объявление No-Contest + +Оракул вызывает `oracle-no-contest` с `market_id` и `reason`. + +1. Все ставки → pending refund payouts (полная исходная сумма) +2. Все позиции LP → pending refund payouts (только принципал) +3. Штраф: `oracle_no_contest_penalty_percent`% от `dispute_fee` из страховки +4. Штраф распределён пропорционально участникам +5. Рынок: `resolved_outcome = -1`, `payout_status = 1` +6. Стартует grace-период (оспариваемый) + +### Резолюция с 3 исходами (спор no-contest) + +Резолвер выбирает одно из: +- `correct_outcome = 0` — A побеждает (пересчёт выплат) +- `correct_outcome = 1` — B побеждает (пересчёт выплат) +- `correct_outcome = -1` — Подтвердить no-contest (сохранить refund-выплаты) + +Если оракул неправ: pending refund payouts удаляются, заменяются корректными выплатами победителям. +Применяются стандартные штрафы спора. + +--- + +## 13. Штраф оракула за пропуск резолюции + +Если оракул не разрешает к `result_expiration`: + +``` +penalty_amount = floor(oracle_insurance × oracle_penalty_percent / 100) +``` + +### Распределение + +``` +stakes[user_id] += bet_amount (for each active bet) +stakes[user_id] += liquidity_amount (for each active LP position) +total_stakes = Σ stakes[user_id] + +bonus_i = floor(penalty_amount × stakes[user_id] / total_stakes) +``` + +Каждый участник получает: полный рефанд (принципал) + пропорциональный бонус. + +Рынок финализирован: status=3, payout_status=2. + +--- + +## 14. Скоринг репутации оракула + +### Сырые метрики (14 счётчиков на оракула) + +| Метрика | Тип | Источник | +|--------|------|--------| +| `markets_accepted` | counter | oracle-accept-market | +| `markets_resolved` | counter | resolve-market | +| `markets_no_contest` | counter | oracle-no-contest | +| `markets_missed` | counter | cron (missed deadline) | +| `disputes_received` | counter | create-dispute | +| `disputes_lost` | counter | resolve-dispute (status=1) | +| `disputes_won` | counter | resolve-dispute (status=2) | +| `disputes_auto_closed` | counter | cron (14-day auto-close) | +| `dispute_responses_missed` | counter | cron (12h response deadline) | +| `total_volume_resolved` | mVIZ | resolve-market (сумма bets_sum) | +| `total_insurance_slashed` | mVIZ | все события штрафов | +| `avg_resolution_time` | seconds | resolve-market | +| `bans_received` | counter | resolve-dispute | +| `active_since` | timestamp | register-oracle | +| `last_active_time` | timestamp | accept/resolve/no-contest | + +### Производные доли + +Знаменатель: `total_outcomes = markets_resolved + markets_no_contest + markets_missed` + +| Доля | Формула | +|------|---------| +| `resolution_rate` | `markets_resolved / total_outcomes` | +| `dispute_loss_rate` | `disputes_lost / disputes_received` | +| `no_contest_rate` | `markets_no_contest / total_outcomes` | +| `deadline_miss_rate` | `markets_missed / total_outcomes` | +| `dispute_response_rate` | `1 − (dispute_responses_missed / disputes_received)` | + +### Reliability Score (0–100) + +``` +reliability_score = clamp(0, 100, + BASE_SCORE + − W_DISPUTE_LOSS × dispute_loss_rate × 100 + − W_NO_CONTEST × excess_no_contest × 100 + − W_DEADLINE_MISS × deadline_miss_rate × 100 + − W_NO_RESPONSE × (1 − dispute_response_rate) × 100 + + W_VOLUME_BONUS × volume_tier + + W_EXPERIENCE × experience_tier × freshness_multiplier + − W_BAN_PENALTY × bans_received +) +``` + +Где `excess_no_contest = max(0, no_contest_rate − 0.10)`. + +### Веса по умолчанию + +| Вес | Значение | +|--------|-------| +| BASE_SCORE | 50 | +| W_DISPUTE_LOSS | 0.40 | +| W_NO_CONTEST | 0.10 | +| W_DEADLINE_MISS | 0.20 | +| W_NO_RESPONSE | 0.15 | +| W_VOLUME_BONUS | 0–25 (тиры: ≥10K→+5, ≥100K→+10, ≥500K→+15, ≥1M→+20, ≥5M→+25) | +| W_EXPERIENCE | 0–25 (тиры: ≥7d→+5, ≥30d→+10, ≥90d→+15, ≥180d→+20, ≥365d→+25) | +| W_BAN_PENALTY | 15 за бан | + +### Freshness Decay + +| Дней с последней активности | Множитель | +|----------------------|-----------| +| ≤ 30 | 1.00 | +| 31–90 | 0.75 | +| 91–180 | 0.50 | +| > 180 | 0.25 | + +### Composite Trust Score + +``` +trust_score = reliability_score × risk_factor +``` + +| Risk score (страховка/ставки) | risk_factor | +|---------------------------|-------------| +| ≥ 3.0× | 1.00 | +| ≥ 2.0× | 0.95 | +| ≥ 1.0× | 0.85 | +| < 1.0× | 0.70 | + +### Детекция нового оракула + +`total_outcomes < 5` → `is_new = true`. Отдельный бейдж в UI. + +Score считается на чтение через `compute_oracle_reliability_score()`, не хранится. + +--- + +## 15. Трансфер позиций + +### Операция + +``` +pm_transfer_position { bet_id, to_user, amount, memo } +``` + +- Перевести все или часть токенов ставки на другой аккаунт +- Переведённые токены сохраняют исходный рынок и исход +- Выплата идёт текущему держателю на резолюции +- Без слиппеджа, без рыночного влияния — чистая переуступка записи +- Работает и для позиций Onix Binary, и Onix Multi + +### Модель приватности memo + +| Режим | Формат | Видимость | +|------|--------|------------| +| Plaintext | Строка, не начинающаяся с `#` | Публично on-chain | +| Encrypted | Строка, начинающаяся с `#` | Приватно — расшифровать могут только отправитель и получатель | + +Шифрование: ECIES shared-secret `ECDH(sender_memo_private, recipient_memo_public)` с использованием +memo-ключей аккаунтов VIZ (стандартная модель Graphene). Шифрование/расшифровка на стороне клиента. + +--- + +## 16. Lazy-пул ликвидности + +### Параметры + +| Медиана-голосуемый параметр | Роль | +|---------|-------------| +| `pm_lazy_pool_enabled` | kill-switch пула | +| `pm_lazy_alloc_percent` | доля свободного баланса, аллоцируемая на рынок (bp) | +| `pm_lazy_max_total_alloc_percent` | кап доли пула на активных рынках (bp) | +| `pm_lazy_recall_step_percent` | шаг graduated-recall на простаивающих рынках (bp) | +| `pm_lazy_lock_sec` | lock-период депозита (секунды) | +| `pm_lazy_emergency_penalty_percent` | штраф на заблокированную прибыль при экстренном выводе (bp) | +| `pm_lazy_min_liquidity_fee_percent` | минимальный `liquidity_fee_percent` рынка (bp) для аллокации пула; ниже него рынок не получает ликвидности пула (порог вознаграждения) | +| `pm_min_liquidity` | мин. аллокация на рынок (он же пол сида рынка) | + +### Депозит + +- Первый депозитор: `shares = amount` +- Последующие: `new_shares = amount × total_shares / free_balance` +- Таймер блокировки: `unlock_time = now + pm_lazy_lock_sec` +- Расчёт наград перед вычислением долей: `pending += shares × (pool.rps − user.snapshot) / PRECISION` + +### Авто-аллокация + +На активации рынка (status → 1): + +``` +alloc_amount = free_balance × allocation_percent / 100 +× (1 − active_market_penalty_pct / 100) ^ oracle_active_market_count +× (1 − fault_penalty_pct / 100) ^ oracle_active_fault_stamps +``` + +Гейт порога вознаграждения (проверяется первым): если у рынка `liquidity_fee_percent < +pm_lazy_min_liquidity_fee_percent`, пул не аллоцирует **ничего** — он субсидирует только рынки, чья +LP-комиссия платит ему достаточно. Единственной ликвидностью остаётся собственный сид создателя. + +Проверки: `alloc_amount ≥ min_market_allocation`, `allocated + alloc_amount ≤ total × max_total_allocation / 100`. + +Pool LP вставляется с `user=0`. Участвует идентично в time-weighted распределении комиссий. + +### Распределение наград (ленивый учёт) + +На резолюции рынка с прибылью pool LP: + +``` +profit = lp_return − allocation_amount +if profit > 0 AND total_shares > 0: + pool.reward_per_share += profit × PRECISION / total_shares +``` + +Награда пользователя (считается на чтение): + +``` +live_reward = pending_rewards + shares × (pool.rps − user.snapshot) / PRECISION +``` + +### Плановый вывод + +Из консолидированной разблокированной записи (полный или частичный): + +``` +1. Run unlock consolidation +2. Settle rewards: pending += shares × (rps − snapshot) / PRECISION +3. Share value = shares_to_burn × free_balance / total_shares +4. Reward portion = pending_rewards × withdraw_percent / 100 +5. Total payout = share value + reward portion +``` + +### Экстренный вывод + +Все депозиты (locked + unlocked): + +``` +1. Settle rewards +2. total_value = shares × free_balance / total_shares + pending_rewards +3. profit = total_value − principal_deposited +4. if profit > 0: penalty = profit × (locked_shares / total_shares) × emergency_penalty / 100 +5. Penalty → pool reward_per_share +6. User receives: total_value − penalty +``` + +### Защита от opportunity-cost + +> **Governance vs хардкод.** Медиана-голосуется только **размер шага** отзыва — +> `pm_lazy_recall_step_percent`. Остальное **захардкожено** (нужен хардфорк): деление на **10 шагов** +> (`window/10`, `check_step ≥ 10`), критерий простоя (шаг простаивает, если *нет новых ставок* с +> прошлой проверки — `bets_sum ≤ bets_sum_at_check`) и **штраф 5% за активный рынок** (`alloc × 95/100`). +> Штраф fault-штампа и его окно истечения тоже захардкожены. + +**A. Graduated Recall:** Длительность рынка делится на **10 фиксированных шагов**. На каждом шаге, если с +прошлой проверки **не пришло новых ставок**, отозвать `pm_lazy_recall_step_percent` (bp, governance) +текущей аллокации в пул. + +**B. Active Market Penalty:** `factor = (1 − 5%) ^ active_market_count` — захардкоженное рекурсивное +снижение на 5% за каждый одновременный активный рынок того же оракула. + +**C. Fault Stamps:** При плохих исходах рынка (no-contest, пропуск дедлайна, нулевой объём, проигрыш +спора, отсутствие ответа, авто-закрытие) оракул получает fault-штамп, авто-истекающий после фиксированного +окна чистой работы; каждый активный штамп дополнительно снижает аллокацию. (Размер штрафа и окно — хардкод.) + +--- + +## 16a. Опциональное плечо (фондируется Lazy-пулом) + +Live с HF14; опционально, управляется медианным kill-switch `pm_leverage_enabled` (по умолчанию off). + +- **Open** (`pm_leverage_open`) — беттер вносит collateral; Lazy-пул **выдаёт займ** маржи из + `free_balance` (ограничено `leverage_fund_used`; проверяется против `pm_leverage_fund_percent`, + `…_max_per_position_bp`, `…_max_position_ratio_percent`, `…_min_market_liquidity` на момент открытия). + Без эмиссии токенов — позиция полностью обеспечена с точки зрения системы. Открытие с плечом **не** + создаёт `pm_bet`; вес кривой держится на `pm_leverage_position_object`. +- **Liquidation** — идёт по **pre-bet резервам**, поэтому пул возвращает `min(cancel_value, + obligation) ≥ loan`: opposing-bet каскад (`pm_place_bet`) и settlement force-close всегда + full-recovery (заём + проценты → пул); **единственный** ограниченный путь bad-debt — same-side + `pm_cancel_bet` (Case B). Каскад **не** гейтится `pm_leverage_enabled` (флаг блокирует только новые + открытия), поэтому выключение плеча никогда не снимает защиту с открытых позиций. +- **Виртуальные операции** — `pm_leverage_resolve` (force-close на расчёте, с исходом + плечом), + `pm_leverage_liquidate` (mid-market, `reason` 0 opposing / 1 cancel). +- **Governance-вес** — депозитчики Lazy-пула сохраняют вес голоса в PM-спорах и DAO-комитете (NAV пула → + vesting-shares через `get_vesting_share_price`, гейт HF14). + +API: `get_account_leverage_positions`, `get_market_leverage_positions`, `get_lazy_pool`. + +## 16b. Batch / Commit-Reveal ставки (анти-MEV) + +Live с HF14 для **бинарных** рынков (мульти форсит `allow_instant_bet`, пока не появится LMSR-батч); +опционально per-market (`allow_batch` / `allow_instant_bet`), медианный kill-switch +`pm_commit_reveal_enabled`. + +- `pm_place_bet` с `mode = 1` ставит **batch**-ставку в очередь; `pm_commit_bet` (commitment hash + + escrow) → `pm_reveal_bet` запускает **commit-reveal** флоу. Нераскрытые commitments форфейтят + `pm_commit_no_reveal_penalty_percent` (bp) через `pm_commit_forfeit`. +- На каждой границе эпохи (`pm_batch_epoch_blocks`, reveal-окно `pm_reveal_window_blocks`) ставки из + очереди сеттлятся по **единой цене** через крон `pm_batch_settle` — AMM двигает только нетто-остаток, + поэтому внутрибатчевый порядок не даёт преимущества, а инвариант `Σ reserve ≥ L` сохраняется. + +## 17. On-chain модель объектов + +Всё состояние живёт в **chainbase-объектах**, зарегистрированных как core-индексы на HF14 — никакой SQL-БД +нет. Определения полей живут в заголовках операций/объектов и доступны +на чтение через [плагин `prediction_market_api`](../plugins/prediction-market-api). Счётчики репутации, +которые прототип держал в `users` table, теперь поля на `pm_oracle_object`. + +| Объект (индекс) | Хранит | Ищется по | +|---|---|---| +| `pm_oracle_object` | регистрацию оракула, страховку, 14 счётчиков репутации, fault-штампы, бан (`banned_until` + `banned_by`) | owner | +| `pm_market_object` | конфиг рынка, CPMM-резервы (`reserve_a/b`, `k`), `*_fee_percent` (bp), `status` / `payout_status`, таймеры, `dispute_mode`, `a_bets_sum` / `b_bets_sum`, заявление оракула о разрешении (`decision_url` / `decision_reason`) | id / creator / oracle / result_expiration | +| `pm_outcome_object` | per-outcome LMSR `q`, `bets_sum`, `bets_count` (мульти-рынки) | market + outcome | +| `pm_bet_object` | ставку — account, `side` / `outcome_index`, `amount`, вес кривой `weight`, `time_penalty`, `status`, `mode` | market / account | +| `pm_liquidity_object` | позицию LP — принципал, время депозита, time-weight; `provider` пуст ⇒ lazy-pool LP | market | +| `pm_commit_object` | commitment-хэш + escrow (batch / commit-reveal) | market / account | +| `pm_dispute_object` | спор — disputer, `proposed_outcome`, escrow комиссии, таймеры, `status`, `dispute_mode`, контраргумент оракула (`oracle_response` / `oracle_response_time`) | market | +| `pm_dispute_vote_object` | один бюллетень комитета — voter, `vote_outcome`, `vote_percent` (изменяем до закрытия) | market + voter | +| `pm_lazy_pool_object` | синглтон-пул — `free_balance` / `allocated_balance` / `earned_balance`, `reward_per_share`, `leverage_fund_used`, `total_shares` | синглтон (id 0) | +| `pm_lazy_deposit_object` | депозитчика — shares, snapshot наград, unlock time | account | +| `pm_lazy_allocation_object` | тихую LP-аллокацию пула в один рынок + состояние graduated-recall (`bets_sum_at_check`, `check_step`, `recalled_amount`) | market | +| `pm_leverage_position_object` | открытую позицию с плечом — collateral, loan, obligation, вес кривой, `status` | account / market + status | +| `pm_creator_ban_object` | забаненного создателя — `banned_until`, `ban_count`, `banned_by` | ban account | + +Метрики репутации считаются на чтение (`compute_oracle_reliability_score()` — §14), не хранятся. Все +процентные поля — базисные пункты (`*_percent`, bp). Аллокации lazy-пула на рынок и состояние +graduated-recall живут на `pm_lazy_allocation_object`; fault-штампы и счётчики репутации оракула — на +`pm_oracle_object`. + +**Только в плагине (не консенсус):** `pm_market_meta_object` — off-chain распарсенные метаданные рынка +(категория / теги / запрещённые юрисдикции) для discovery и фильтрации по юрисдикции; строится плагином +[`prediction_market_api`](../plugins/prediction-market-api) из непрозрачной строки `metadata` рынка и +никогда не участвует в консенсусе. + +Полные определения полей этих объектов см. в +[Операциях прогнозных рынков](../protocol/operations/prediction-markets). diff --git a/@l10n/ru/docs/prediction-markets/whitepaper.md b/@l10n/ru/docs/prediction-markets/whitepaper.md new file mode 100644 index 0000000000..1f54c8e96d --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/whitepaper.md @@ -0,0 +1,807 @@ +--- +title: Onix Protocol — Whitepaper +description: "Индустриальный whitepaper протокола Onix: прогнозные рынки с гарантированной ликвидностью на VIZ DLT." +--- + +# Onix Protocol: прогнозные рынки с гарантией LP на VIZ DLT + +**Индустриальный whitepaper** + +*Анатолий Пискунов (On1x)* +*Версия 2.0 — июнь 2026 (on-chain / HF14)* + +--- + +> **On-chain статус (HF14).** Этот документ изначально писался против централизованного прототипа. +> Теперь протокол работает как **операции консенсуса первого класса (`pm_*`) на VIZ DLT**, проверено в +> `consensus_sim`. Live с HF14: оба типа рынков (CPMM binary + LMSR multi), parimutuel zero-sum +> расчёт, **Lazy Pool**, опциональный сабсистем **плеча**, опциональные **batch / commit-reveal +> ставки** (бинарные), bonded-оракулы и двухрежимная система споров (комитет / аккаунт). Все +> процентные параметры — в **базисных пунктах (bp): 10000 = 100.00%** (прототип использовал permille). +> Разделы ниже снабжены пометками там, где живой дизайн отличается от исходного текста прототипа. + +## Аннотация + +Прогнозные рынки агрегируют рассеянную информацию в цены, давая оценки вероятностей, которые стабильно +превосходят опросы, экспертные панели и статистические модели. Но внедрение упирается в одну структурную +проблему: **поставщики ликвидности теряют деньги.** + +LP в Uniswap v3 страдают от impermanent loss. Маркет-мейкеры LMSR рискуют всей своей субсидией. +Маркет-мейкеры CLOB сталкиваются с adverse selection. Каждая существующая модель просит поставщиков +капитала принять downside-риск в обмен на неопределённую доходность — и данные показывают, что +большинство из них теряет. + +**Onix Protocol** устраняет риск LP полностью. Это архитектура прогнозного рынка, где принципал LP +**структурно гарантирован** — не страховкой, не хеджированием, а самой механикой выплат. Победителям +платят исключительно из проигранных ставок. Капитал LP даёт глубину рынка, но никогда не используется для +расчёта по ставкам. + +Документ описывает два типа рынков Onix — **Onix Binary** (Constant Product Market Maker) и **Onix Multi** +(LMSR-ценообразование с parimutuel-расчётом) — а также архитектуру рынка, систему оракулов и разрешения +споров, lazy-пул ликвидности, опциональное плечо, модель управления и реализацию на VIZ DLT как операций +уровня консенсуса. + +--- + +## 1. Проблема: риск LP в прогнозных рынках + +Любому прогнозному рынку нужна ликвидность. Без неё цены бессмысленны — ставка, двигающая рынок на 20%, +раскрывает капитал беттера, а не мудрость толпы. Фундаментальный вопрос: **кто даёт эту ликвидность и чем +рискует?** + +### 1.1 Текущий ландшафт + +| Платформа | Модель LP | Риск LP | Источник дохода | +|----------|----------|---------|-------------| +| **Uniswap v3** | Концентрированный AMM | Impermanent loss (часто >5% годовых; >50% LP v3 проигрывают buy-and-hold) | Торговые комиссии | +| **Aave / Compound** | Lending-пул | Риск смарт-контракта, каскады ликвидаций | Проценты заёмщика | +| **Curve** | Stableswap AMM | Низкий IL для привязанных активов, риск смарт-контракта | Комиссии + эмиссия CRV | +| **Standard LMSR** | Субсидия маркет-мейкера | Убыток до `b × ln(N)` — вся субсидия | Bid-ask спред | +| **Polymarket (CLOB)** | Активный маркет-мейкинг | Инвентарный риск, adverse selection | Bid-ask спред | +| **Kalshi** | Концепции LP нет | N/A (биржевая модель) | N/A | + +Закономерность очевидна: предоставление ликвидности прогнозным рынкам требует либо навыка активного +управления (CLOB), либо терпимости к потере капитала (LMSR), либо принятия impermanent loss (AMM). Ни +одно из этого не подходит ритейл-участникам. + +### 1.2 Почему это важно + +Прогнозные рынки работают лучше всего, когда они глубокие и ликвидные. Глубокие рынки дают точные цены, +привлекают информированных трейдеров и генерируют информационную ценность, делающую прогнозные рынки +полезными как общественное благо. Но глубина требует капитала, а капитал требует компенсации за риск. + +Результат — проблема курицы и яйца: +- Тонкие рынки → высокий слиппедж → плохой UX → мало бетторов → низкие комиссии → нет стимула для LP → тонкие рынки + +Чтобы разорвать этот цикл, нужно убрать риск со стороны LP. Если предоставление ликвидности безрисковое, +барьер входа падает до нуля, и маховик может раскрутиться. + +--- + +## 2. Onix Protocol + +### 2.1 Принципы дизайна + +Onix Protocol построен на трёх архитектурных инвариантах: + +1. **Принципал LP структурно безопасен.** Это не стратегия снижения риска — это свойство архитектуры + выплат. Капитал LP и расчёт по ставкам берутся из физически раздельных пулов. + +2. **Проигравшие финансируют победителей.** Все выплаты (прибыль победителей, комиссии оракула, комиссии + создателя, комиссии LP) берутся исключительно из проигранных ставок. Комиссии считаются на резолюции + как `floor(losers_sum × fee_bp / 10000)` (bp: 10000 = 100.00%), никогда не удерживаются при ставке. + +3. **Два типа рынков, одна гарантия.** Бинарные рынки (Onix Binary) и мульти-исходные рынки (Onix Multi) + используют разные формулы ценообразования, но разделяют одну модель расчёта и одну гарантию LP. + +### 2.2 Onix Binary (CPMM + parimutuel-расчёт) + +Onix Binary использует формулу Constant Product Market Maker — тот же инвариант `x * y = k`, что и +Uniswap — как **движок ценообразования** для бинарных исходов, с **parimutuel-расчётом** (проигравшие +финансируют победителей пропорционально весу), той же моделью расчёта, что и в Onix Multi. + +**Механика:** + +Рынок поддерживает два резерва, `reserve_a` и `reserve_b`, с постоянным произведением `k`: + +``` +k = reserve_a × reserve_b +``` + +Когда пользователь ставит `amount` на исход A (в реализации side 0 → `reserve_a`), стейк входит в резерв +этой стороны, а токены берутся из **противоположного** резерва: + +``` +new_reserve_a = reserve_a + amount +new_reserve_b = floor(k / new_reserve_a) +tokens_received = reserve_b − new_reserve_b +``` + +`tokens_received` (называемые `weight`) — это **относительная претензия** пользователя на пул победителей, +если выигрывает исход A (расчёт parimutuel — см. ниже, идентично Onix Multi). Подразумеваемая вероятность +растёт для той стороны, на которую ставят (больше денег на A → `reserve_a` растёт → `P(A)` растёт): + +``` +P(A) = reserve_a / (reserve_a + reserve_b) +P(B) = reserve_b / (reserve_a + reserve_b) +``` + +**Расчёт и доказательство безопасности LP (parimutuel):** + +На резолюции победители получают назад свой стейк плюс пропорциональную долю пула проигравших, по весу +(идентично Onix Multi): + +``` +winners_pool = losers_sum − fees +payout = bet_amount + (weight / total_winning_weight) × winners_pool − time_penalty_on_profit +``` + +Принципал LP `L` возвращается безусловно, и гарантия точна: + +``` +Money OUT = L + winning_bets + winners_pool + fees = L + winning_bets + losing_bets = L + all_bets = Money IN +``` + +Суммарная выплата ограничена `losers_sum` независимо от весов, поэтому капитал LP никогда не используется +для расчёта по ставкам. CPMM — это **движок ценообразования** (вероятность + вес); он не гейтит выплату. +(Соотношение AM-GM `reserve_a + reserve_b ≥ 2√k = L` по-прежнему держится для ценовой кривой, но на него +больше не опираются для платёжеспособности.) + +**Разобранный пример:** + +``` +Setup: 200 VIZ liquidity → reserve_a = 100, reserve_b = 100, k = 10,000 +Fees (bp): oracle 50 (0.5%), creator 50 (0.5%), liquidity 100 (1%) + +Alice bets 50 VIZ on A → receives weight 33.33 (price moves from 50% to 69%) +Bob bets 80 VIZ on B → receives weight 81.82 + +Resolution: A wins + Losers (Bob): 80 VIZ forfeited → losers_sum = 80 + oracle_fee = floor(80 × 50/10000) = 0.4 VIZ + creator_fee = floor(80 × 50/10000) = 0.4 VIZ + liq_fee = floor(80 × 100/10000) = 0.8 VIZ + winners_pool = 80 − 1.6 = 78.4 VIZ + + Alice (only winner, weight 33.33 of 33.33): + payout = 50 (stake) + 78.4 × (33.33/33.33) = 128.4 VIZ (minus any time penalty on profit) + LP return: 200 VIZ principal + share of 0.8 VIZ fee pool +``` + +### 2.3 Onix Multi (LMSR + parimutuel-расчёт) + +Onix Multi — инновация протокола для рынков с 3–10 исходами. Он сочетает Logarithmic Market Scoring Rule +Хэнсона (LMSR, 2003) для ценообразования в реальном времени с parimutuel-расчётом ради безопасности LP. + +**Ценообразование (LMSR softmax):** + +Для рынка с исходами {1, 2, ..., N}, каждый со своим параметром количества `q_i`: + +``` +price(i) = exp(q_i / b) / Σ_j exp(q_j / b) +``` + +Это функция softmax — цены всегда суммируются ровно в 1.0 by construction. Никакого механизма арбитража +или операции split/merge не нужно. + +Стоимость покупки Δ токенов на исход i: + +``` +C(q) = b × ln(Σ_j exp(q_j / b)) + +cost = C(q + Δ·e_i) − C(q) +``` + +Параметр `b` управляет чувствительностью цены (выше b = меньше price impact на ставку). Он фондируется +субсидией LP: `b = S / ln(N)`, где S — суммарная субсидия. + +**Инновация — parimutuel-расчёт:** + +В **стандартном LMSR** маркет-мейкер — контрагент всех ставок. Если толпа верно предсказывает исход, +маркет-мейкер теряет до `b × ln(N)` — потенциально всю субсидию. Именно поэтому LMSR мало внедряли вне +корпоративных прогнозных рынков (Microsoft, Inkling), где оператор поглощает убыток. + +**Onix Multi меняет источник выплат.** На резолюции: + +``` +1. Oracle declares the winning outcome +2. Losers forfeit 100% → losers_sum +3. Fees deducted from losers_sum (bp; 10000 = 100.00%): + oracle_fee = floor(losers_sum × oracle_fee_bp / 10000) + creator_fee = floor(losers_sum × creator_fee_bp / 10000) + liq_fee = floor(losers_sum × liquidity_fee_bp / 10000) + winners_pool = losers_sum − fees +4. Winners receive: + payout = bet_amount + (tokens / total_winning_tokens × winners_pool) − time_penalty +5. LP subsidy returned unconditionally +``` + +Победителям платят проигравшие, а не LP. Субсидия архитектурно отделена от потока расчёта. + +**Доказательство гарантии принципала LP:** + +1. LP вносит `S` VIZ как субсидию, которая фондирует глубину рынка. +2. Во время ставок пользователи платят VIZ → получают токены исходов. VIZ накапливается как пул ставок. +3. На резолюции проигранные ставки фондируют выплаты победителям и комиссии. Субсидия `S` никогда не была + в платёжном пуле. +4. Субсидия возвращается LP безусловно, независимо от исхода. + +**Сравнение:** + +| Измерение | Standard LMSR | Onix Multi | +|-----------|---------------|------------| +| Роль LP | Контрагент всех ставок | Депозит глубины (не контрагент) | +| Макс. убыток LP | `b × ln(N)` (вся субсидия) | **Ноль** | +| Выплата победителю | 1 токен = 1 единица валюты | Токен = пропорциональная претензия на пул проигравших | +| Нужен ли CTF split/merge? | Да (обеспечить сумму цен = 1) | Нет (softmax гарантирует это) | + +**Разобранный пример (выборы с 3 исходами):** + +``` +Setup: b = 1000, outcomes = [A, B, C], subsidy = 1000 VIZ +Initial: price(A) = price(B) = price(C) = 33.3% + +Alice bets 50 VIZ on A → ~47 tokens (price: 33% → ~38%) +Bob bets 100 VIZ on B → ~88 tokens +Carol bets 30 VIZ on C → ~29 tokens + +Resolution: A wins + Losers: Bob (100) + Carol (30) = 130 VIZ + Fees (200 bp = 2% total): 2.6 VIZ + winners_pool = 127.4 VIZ + + Alice: 50 + (47/47 × 127.4) = 177.4 VIZ + LP: 1000 VIZ returned in full + share of liquidity fees +``` + +### 2.4 Граничные случаи + +| Сценарий | Исход | +|----------|---------| +| Все ставки на победителя | `losers_sum = 0` → каждый беттер получает назад ровно свою ставку. Субсидия LP возвращена. Zero-sum. | +| Нет ставок на победителя | Весь пул проигравших нераспределён → бонус LP. LP в максимальном плюсе. | +| Рынок с нулевым объёмом | Субсидия LP возвращена полностью. Ни комиссий, ни выплат. | +| Выигрывает единственный беттер | Этот беттер получает `bet_amount + winners_pool`. Субсидия LP возвращена. | + +--- + +## 3. Архитектура рынка + +### 3.1 Жизненный цикл рынка + +```mermaid +stateDiagram-v2 + direction LR + state "Waiting (0)" as Waiting + state "Active (1)" as Active + state "Closed (2)" as Closed + state "Resolved (3)" as Resolved + state "Deleted (-1)" as Deleted + state "Paid out" as Paid + [*] --> Waiting + Waiting --> Active: оракул принял + Waiting --> Deleted: оракул отклонил + Active --> Closed: ставки закрылись + Active --> Resolved: раннее разрешение (если разрешено) + Closed --> Resolved: оракул разрешает + Resolved --> Paid: окно ожидания (12ч) + Deleted --> [*] + Paid --> [*] +``` + +Рынки создаёт создатель рынка, проверяет и принимает оракул (ставящий страховку), они открыты для ставок, +разрешаются исходом и выплачиваются после grace-периода для споров. + +### 3.2 Модель комиссий (извлечение только из проигравших) + +Отличительная черта Onix Protocol — **комиссии не удерживаются при ставке**. Полная сумма ставки входит в +резервы рынка. Комиссии считаются только на резолюции, исключительно из проигранных ставок: + +```mermaid +flowchart TD + LS["losers_sum (100% проигранных ставок)"] + LS --> OF["oracle_fee = floor(losers_sum × oracle_fee_bp / 10000)"] + LS --> CF["creator_fee = floor(losers_sum × creator_fee_bp / 10000)"] + LS --> LF["liquidity_fee = floor(losers_sum × liquidity_fee_bp / 10000)"] + LS --> WP["winners_pool = losers_sum − все комиссии"] +``` + +Это даёт структурную гарантию: комиссии и выплаты победителям берутся из совершенно раздельных источников. +Извлечение комиссий никогда не конкурирует с обязательствами перед победителями. + +**Условия комиссии оракула фиксируются при акцепте (offer→quote).** Создатель публикует *максимум*, +который оракул может взять (потолок `oracle_fee_percent` в bp + потолок `oracle_fixed_fee`); при акцепте +оракул котирует свои фактические условия (≤ потолка создателя и ≤ медианного governance-капа +`pm_max_oracle_fee_percent`), которые замораживаются на рынке, и эмитится виртуальная операция +`pm_market_accepted`. Self-оракул фиксирует условия при создании. **Фиксированная комиссия оракула** +(на рынок) выплачивается из остатка пула проигравших (никогда не печатается). **Комиссия за создание +рынка** идёт в фонд DAO как защита от спама. + +### 3.3 Time-weighted распределение LP + +Доли комиссий LP распределяются пропорционально `amount × max(1, seconds_to_expiration)`: + +``` +weight_i = amount_i × max(1, sec_to_expiration_i) +fee_share_i = floor(total_fee_pool × weight_i / Σ weight_j) +``` + +Ранние LP зарабатывают драматически больше на единицу капитала, чем поздние. В 48-часовом рынке LP, +внёсший на 1-м часу, зарабатывает ~2400× больше на VIZ, чем внёсший на 47-м. + +Каждый депозит отслеживается как независимая позиция — несколько депозитов одного пользователя взвешиваются +и оплачиваются раздельно. Принципал LP всегда возвращается полностью, независимо от исхода рынка. + +### 3.4 Time penalty для поздних ставок + +Чтобы дестимулировать ставки в последнюю минуту (несущие меньше риска неопределённости), к ставкам близ +экспирации применяется настраиваемый time penalty: + +``` +if time_to_expiration < penalty_window: + ratio = 1 − (time_to_expiration / penalty_window) + penalty_ratio = ratio² // quadratic (default) + time_penalty = floor(penalty_ratio × max_penalty) +``` + +Штраф применяется **только к прибыли**, никогда к принципалу. Победивший беттер всегда получает не меньше +своей исходной ставки. Квадратичная кривая мягкая в начале окна штрафа и крутая в конце, награждая +«немного поздно» над «очень поздно». + +### 3.5 Трансфер позиций + +Позиции переводимы между аккаунтами через нативную операцию протокола: + +``` +pm_transfer_position { bet_id, to_user, amount, memo } +``` + +Без слиппеджа, без рыночного влияния — чистая переуступка записи. Поле `memo` поддерживает и открытый, и +зашифрованный режим (ECIES через memo-ключи аккаунтов VIZ), позволяя P2P-сделки, OTC-торговлю и приватные +аннотации. + +Это единственная фича композируемости из Conditional Tokens Framework (Polymarket/Gnosis), дающая реальную +пользу. CTF split/merge архитектурно не нужен — формулы ценообразования Onix гарантируют когерентность цен +by construction. + +--- + +## 4. Оракул и разрешение споров + +### 4.1 Модель bonded-оракула + +Оракулы в Onix Protocol не доверены по умолчанию — они **bonded**. Каждый оракул обязан: + +- Зарегистрироваться с разовой комиссией (по умолчанию 10 VIZ) +- Внести страховку (минимум 5000 VIZ) +- Явно принимать рынки (ставя свою страховку на каждый акцепт) +- Разрешать рынки исходом и подтверждающими доказательствами (decision URL) + +Страховой бонд создаёт подотчётность: оракулы, которые неверно разрешают, пропускают дедлайны или +проигрывают споры, теряют часть страховки (slashing). Бонд должен превышать потенциальную прибыль +манипуляции оракула, чтобы модель экономической безопасности держалась. + +Доход оракула из двух источников: +1. **Фиксированная комиссия** (на рынок) — компенсирует ставку страховки и предоставление резолюции +2. **Процентная комиссия** (из пула проигравших на резолюции) — масштабируется с объёмом рынка + +### 4.2 Арбитраж споров + +Любой беттер может оспорить резолюцию в течение grace-периода, заплатив dispute fee. Во время споров все +выплаты заморожены. + +Разрешение идёт в одном из **двух per-market режимов**, выбираемом при создании: + +- **Режим комитета (`dispute_mode = 0`, по умолчанию)** — *весь электорат SHARES* решает + **stake-weighted голосованием** (`pm_dispute_vote`), детерминированно подсчитываемым кроном + `pm_dispute_finalize` на `voting_end_time`. Это **открытые публичные слушания**: живой тали + запрашиваем, и голоса **не** скрыты за commit-reveal (намеренный, постоянный выбор — DAO разрешает + споры максимально прозрачно). Поскольку по ходу слушаний всплывают новые доводы, **бюллетень изменяем** + до закрытия (повторный голос перезаписывает прежний). Вес голосующего — его `effective_vesting_shares` + **плюс стейк Lazy-Pool, конвертированный в vesting-shares**, поэтому участники DAO, паркующие VIZ в + пуле, сохраняют свой вес в управлении. +- **Режим аккаунта (`dispute_mode = 1`)** — единственный именованный `dispute_resolver` + (рекомендуется multisig) выносит вердикт (`pm_dispute_resolve`). + +Логика вердикта в любом режиме: + +**Если оракул был неправ (переворот):** +- Применяется верный исход, выплаты пересчитываются. +- Диспутёр получает назад свою fee **плюс carve-out награды** из слешнутой страховки, размером + `dispute_fee × pm_dispute_reward_multiplier` (bp; напр. 30000 = ×3), ограниченный слэшем. +- **Остаток слэша добавляется в пул победителей** (через `forfeit_pool`) — он идёт выигравшим бетторам, + **не** резолверу и **не** в ДАО. Ни голосующие комитета, ни аккаунт-резолвер награды **не получают** + (голосование комитета — неоплачиваемая governance-обязанность). +- Страховка оракула слешится (масштаб — по силе консенсуса в режиме комитета или по `penalty_amount` + резолвера в режиме аккаунта), с опциональным баном. + +**Если оракул был прав (подтверждён):** +- Диспутёр **отдаёт всю dispute fee оракулу** (компенсация за недобросовестное оспаривание). +- Исходные выплаты идут без изменений. + +**Предотвращение denial-of-resolution:** Если резолвер не действует в течение 14 дней, споры +авто-закрываются: все ставки и LP возвращаются, оракул штрафуется, fee диспутёра возвращается. Это +гарантирует, что средства никогда не заморожены бессрочно. + +### 4.3 Скоринг репутации оракула + +Протокол отслеживает 14 on-chain метрик на оракула и считает reliability score (0–100): + +``` +reliability_score = clamp(0, 100, + 50 (base) + − 0.40 × dispute_loss_rate × 100 + − 0.10 × excess_no_contest × 100 + − 0.20 × deadline_miss_rate × 100 + − 0.15 × (1 − dispute_response_rate) × 100 + + volume_bonus (0–25) + + experience_bonus × freshness_multiplier (0–25) + − 15 × bans_received +) +``` + +Ключевые дизайн-решения: +- **Доли, а не счётчики** — 1 проигранный спор из 100 (1%) лучше, чем 1 из 2 (50%) +- **Нейтральный старт на 50** — новые оракулы должны заработать репутацию, а не стартовать со 100 +- **Freshness decay** — неактивные оракулы со временем теряют experience-бонус +- **Тиры объёма** — высокообъёмные оракулы получают бонусные очки за доказанный track record + +Reliability score сочетается с risk factor (отношение страховки к ставкам), давая **composite trust +score** — основную метрику, показываемую пользователям. + +### 4.4 No-contest и резолюция с 3 исходами + +Оракул, который не может верифицировать исход, может добровольно объявить **no-contest**, запуская +рефанды с пониженным штрафом (50% dispute fee из страховки — гораздо дешевле, чем проиграть спор). Это +создаёт градиент стимулов: + +| Сценарий | Стоимость для оракула | Риск бана | +|----------|------------|----------| +| Добровольный no-contest | 500 VIZ | Нет | +| Проигрыш спора | 1000+ VIZ + доп. штраф | Постоянный или временный | +| Пропущенный дедлайн | 250 VIZ (авто-штраф) | Нет (но урон репутации) | + +Если пользователи считают, что оракул злоупотребил no-contest, они могут это оспорить. Тогда резолвер +выбирает из **трёх** возможных верных исходов: побеждает A, побеждает B или подтвердить no-contest. Это +не даёт оракулам использовать no-contest, чтобы избежать выплат победившим бетторам. + +--- + +## 5. Lazy-пул ликвидности + +### 5.1 Проблема развёртывания капитала + +Индивидуальное предоставление LP требует активного выбора рынков. Большинство пользователей не станут +вручную оценивать конкретные рынки и вкладываться в них. Итог: большинство рынков стартует только с +начальной ликвидностью создателя, давая тонкие книги и высокий слиппедж. + +### 5.2 Авто-аллокация пул→рынок + +Lazy-пул ликвидности решает это, принимая депозиты и **автоматически аллоцируя** процент свободного +баланса пула в каждый новый рынок при его активации: + +``` +alloc_amount = free_balance × allocation_percent / 100 +``` + +Аллокации считаются от текущего свободного баланса (не от исходной суммы), создавая геометрический спад — +пул никогда нельзя полностью истощить: + +``` +After 50 markets (2% allocation each): ~357 VIZ free from original 1,000 +After 100 markets: ~133 VIZ still free +``` + +Максимальный кап суммарной аллокации (по умолчанию 70%) даёт дополнительную безопасность. + +### 5.3 Распределение наград (один общий аккумулятор) + +Проблема: когда рынок разрешается с прибылью пула, эту прибыль надо разделить между **всеми** текущими +вкладчиками пропорционально их долям — но проходить по каждому вкладчику на каждом рынке было бы O(N) и +неограниченно. Пул избегает этого **одним глобальным бегущим итогом** `reward_per_share` («rps»): + +``` +// When a market resolves with pool LP profit, the per-share value of the pool rises once: +pool.reward_per_share += profit × PRECISION / total_shares + +// A depositor's earnings = their shares × how much rps has risen since they last touched the pool: +live_reward = pending + shares × (pool.reward_per_share − user.snapshot) / PRECISION +``` + +Простыми словами: каждый вкладчик «владеет» долей каждого роста `reward_per_share`, и его награда = +`доли × (текущий rps − rps на момент его последнего депозита/вывода)`. Запись самого вкладчика трогается +**только когда он действует** (депозит/вывод); до тех пор его право копится тихо в глобальном числе. Поэтому +раздача прибыли тысячам вкладчиков — **O(1)** (одно сложение), и ничего не выплачивается до клейма. Это +известный паттерн-аккумулятор из +[контракта MasterChef SushiSwap](https://github.com/sushiswap/masterchef/blob/master/contracts/MasterChef.sol) +(и индекса cToken у Compound); `PRECISION` (1e9) держит целочисленное деление точным. + +### 5.4 Защита от opportunity-cost + +Пул авто-аллоцирует в каждый рынок, создавая вектор атаки: вредоносный оракул мог бы создавать длинные +рынки с нулевым объёмом, чтобы запереть капитал пула. Это адресуют три механизма: + +**Graduated Early Recall:** Длительность рынка делится на 10 шагов. На каждом шаге, если объём ставок ниже +порога (1% аллокации), 10% текущей аллокации отзывается в пул. Полностью простаивающий 30-дневный рынок +теряет ~60% своей аллокации. + +**Active Market Penalty:** Каждый дополнительный активный рынок того же оракула снижает его аллокацию на +5% (рекурсивно). Оракул с 10 активными рынками получает ~60% базовой аллокации на рынок, стимулируя +качество над количеством. + +**Fault Penalty Stamps:** Плохие исходы (пропущенные дедлайны, проигранные споры, резолюции с нулевым +объёмом) генерируют штрафные штампы, дополнительно снижающие будущие аллокации. Штампы авто-истекают после +10 дней чистой работы. + +### 5.5 Опциональное плечо (фондируется Lazy-пулом) + +Lazy-пул играет **две роли из одного `free_balance`**: тихие market-LP аллокации *и* фондирование +сабсистемы **опционального плеча**. Беттер может открыть позицию с плечом (`pm_leverage_open`), где маржа +— это **займ из пула**: без эмиссии токенов, позиция остаётся полностью обеспеченной с точки зрения +системы. Бинарный «jump risk», ломающий liquidation-движки CLOB, решается **ликвидацией по pre-bet +резервам**: opposing-bet или settlement force-close возвращает `min(cancel_value, obligation) ≥ loan`, +поэтому пул получает назад заём плюс проценты; единственный ограниченный путь bad-debt — same-side +`pm_cancel_bet`. Медианный kill-switch (`pm_leverage_enabled`, по умолчанию off) блокирует *новые* +открытия, но защитный каскад ликвидации намеренно **не** гейтится им — выключение плеча никогда не снимает +защиту с открытых позиций. Пул зарабатывает проценты по плечу вдобавок к LP-доходности, учёт тем же +способом MasterChef. Расчёт по займам пула эмитит `pm_leverage_resolve` / `pm_leverage_liquidate`. + +--- + +## 6. Управление + +### 6.1 Делегат-голосуемые параметры цепи + +VIZ использует консенсус Delegated Proof of Stake (DPoS), где избранные делегаты (валидаторы) управляют +параметрами цепи через механизм медианного голоса: + +1. Каждый делегат публикует предпочитаемые значения всех параметров +2. Сеть вычисляет **медиану** голосов всех активных делегатов +3. Параметры меняются автоматически при сдвиге медианы — без хардфорка, без деплоя + +Все параметры прогнозных рынков (комиссии, штрафы, требования к страховке, окна споров, настройки +lazy-пула, ручки плеча, тайминг batch/commit-reveal) делегат-голосуемы. Все процентные параметры — в +**базисных пунктах (bp), 10000 = 100.00%**: + +| Примеры | Управление | +|----------|-----------| +| `pm_dispute_fee`, `pm_max_oracle_fee_percent` (bp) | Медианный голос делегатов | +| `pm_dispute_grace_sec`, `pm_dispute_vote_period_sec` | Медианный голос делегатов | +| `pm_dispute_approve_min_percent`, `pm_dispute_reward_multiplier` (bp) | Медианный голос делегатов | +| `pm_lazy_*` аллокация/recall, `pm_leverage_*` (enabled, fund %, max position) | Медианный голос делегатов | +| `pm_commit_reveal_enabled`, `pm_batch_epoch_blocks`, `pm_reveal_window_blocks` | Медианный голос делегатов | + +Хардфорки нужны только для структурных изменений (новые типы операций, изменения формул), не для +экономической настройки. Kill-switches (`pm_leverage_enabled`, `pm_commit_reveal_enabled`) позволяют +управлению отключить целую подсистему медианным голосом без форка. + +### 6.2 Модель юрисдикционного клиента + +VIZ DLT — инфраструктура, не оператор — аналогично тому, как Bitcoin это леджер, а не money transmitter. +Протокол нейтрален и permissionless. Правовые обязательства привязаны к **клиентским приложениям**, не к +алгоритму консенсуса. + +Любая юрисдикция может построить compliant-клиент на VIZ DLT: + +| Компонент клиента | Реализация | +|-----------------|---------------| +| Предодобренные оракулы | Whitelist клиента из лицензированных, KYC-верифицированных оракулов | +| Предодобренные резолверы | Госорганы разрешения споров | +| KYC/AML | Идентификация на уровне клиента | +| Маршрутизация комиссий как налоговый доход | `dao_fund_account_id` → счёт госказны | +| Ограничения рынков | Клиент фильтрует по разрешённым категориям | +| Лимиты ставок | Лимиты на пользователя, навязанные клиентом | + +Те же операции протокола (`pm_place_bet`, `pm_resolve`, `pm_dispute`) работают одинаково для +permissionless и регулируемых клиентов. Разница целиком на слое клиента. + +--- + +## 7. Конкурентный ландшафт + +### 7.1 Сравнение платформ + +| Измерение | Onix (Forecaster) | Polymarket | Kalshi | Standard LMSR | +|-----------|-------------------|------------|--------|---------------| +| **Ценообразование** | CPMM (binary) / LMSR softmax (multi) | CLOB | CLOB | LMSR | +| **Риск LP** | **Ноль** (структурная гарантия) | Инвентарный риск | N/A | До `b × ln(N)` | +| **Знания LP** | Низкие (внёс и зарабатывай) | Высокие (управляй ордерами) | N/A | Средние | +| **Модель комиссий** | % пула проигравших на резолюции | Bid-ask спред | Биржевые комиссии (1-7%) | Спред | +| **Оракул** | Per-market bonded + диспут комитета | UMA Optimistic Oracle | Kalshi (регулятор CFTC) | Оператор | +| **Штраф за поздние ставки** | Квадратичный, настраиваемый | Нет | Нет | Нет | +| **Трансфер позиций** | Нативная операция + зашифрованное memo | CTF (ERC-1155) | Нет | Нет | +| **Управление** | Делегат-голосуемые параметры | Multisig команды | Процесс CFTC | Оператор | +| **Инфраструктура** | VIZ DLT (уровень консенсуса) | Polygon (смарт-контракты) | Проприетарные серверы | Разное | + +### 7.2 Почему CTF split/merge не нужен + +Polymarket использует Gnosis Conditional Tokens Framework (CTF), где позиции — это ERC-1155 токены, +которые можно split и merge для обеспечения когерентности цен (сумма цен = $1). + +В Onix Protocol этот механизм архитектурно не нужен: + +- **Onix Binary (CPMM):** `price(A) + price(B) = reserve_b/(reserve_a+reserve_b) + reserve_a/(reserve_a+reserve_b) = 1` — по определению +- **Onix Multi (LMSR softmax):** `Σ price(i) = Σ exp(q_i/b) / Σ exp(q_j/b) = 1` — по определению softmax + +Механизм арбитража не нужен. Когерентность цен — математическое свойство формул, а не внешний слой +энфорсмента. + +### 7.3 Маховик + +``` +Risk-free LP → lower barrier for retail LPs + → more liquidity deposited + → deeper markets, less slippage + → better UX for bettors + → more volume + → more fees for LPs + → attracts even more LPs +``` + +«Пассивная доходность без impermanent loss» — ценностное предложение, которого Uniswap, Balancer и Curve +дать не могут. Для crypto-native аудитории это убедительный нарратив: зарабатывай доход, предоставляя +ликвидность прогнозным рынкам, с нулевым риском для принципала. + +--- + +## 8. VIZ DLT: от прототипа к протоколу + +### 8.1 Текущее состояние + +Протокол начинался как Telegram WebApp с централизованным бэкендом (вся логика рынка на сервере) — +рабочий прототип с известными ограничениями: нет sybil-устойчивости сверх Telegram-аккаунтов, нет +цензуроустойчивости, нет композируемости. **Эта миграция теперь выполнена:** полная логика рынка работает +**на VIZ DLT как consensus-validated операции `pm_*`** (HF14), отработана end-to-end в `consensus_sim`. +Остаток раздела описывает эту on-chain архитектуру, теперь реализованную. + +### 8.2 Архитектура миграции + +VIZ DLT — Distributed Ledger Technology с ~3-секундными блоками, DPoS-консенсусом, именованными аккаунтами +(в стиле Graphene) и без general-purpose смарт-контрактов. Операции прогнозного рынка реализованы как +**операции консенсуса первого класса** — не смарт-контракты, не `custom_json`-нагрузки. + +| Слой | Примеры | Validated консенсусом? | +|-------|---------|---------------------| +| **Операции протокола** | `pm_create_market`, `pm_oracle_accept_market`, `pm_place_bet`, `pm_commit_bet`/`pm_reveal_bet`, `pm_resolve_market`, `pm_dispute_create`/`pm_dispute_vote`/`pm_dispute_resolve`, `pm_lazy_deposit`/`pm_lazy_withdraw`, `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert` | Да — валидирует каждый узел | +| **Виртуальные операции** | `pm_payout` (на беттера), `pm_auto_payout`, `pm_market_accepted`, `pm_dispute_finalize`, `pm_dispute_auto_close`, `pm_oracle_missed_penalty`, `pm_lazy_recall`, `pm_batch_settle`, `pm_commit_forfeit`, `pm_leverage_resolve`/`pm_leverage_liquidate` | Да — детерминированы, генерируются на этапе блока | +| **metadata / custom_json** | Комментарии споров, описания рынков, UI-метаданные | Нет — только отображение/индексация | + +Каждое финансовое действие (ставки, добавление ликвидности, резолюция рынков, изъятие страховки) +валидируется каждым валидатором. Невалидные операции отвергаются до включения в блок. Никакого Solidity, +оценки газа, деплоя байткода. + +Фронтенд — **полностью headless web-клиент** — без бэкенд-сервера, без БД, без сессий. Приватные ключи в +браузере (зашифрованы), транзакции подписываются локально и вещаются на публичные узлы VIZ. Нет зависимости +от Telegram; ядро приложения платформонезависимо. + +### 8.3 Реализовано с момента прототипа и оставшаяся дорожная карта + +**Реализовано on-chain (HF14):** + +| Фича | Статус | +|---------|--------| +| Commit-reveal + batch ставки (бинарные, опциональные, медианный kill-switch) | ✅ Live | +| Опциональное плечо (фондируется Lazy-пулом, ликвидация по pre-bet резервам) | ✅ Live | +| Lazy Pool (авто-аллокация, graduated recall, MasterChef-учёт) | ✅ Live | +| Виртуальные операции расчёта на беттера / плечо + API плагина | ✅ Live | +| Стейк lazy-пула как governance-вес (PM-споры + заявки DAO) | ✅ Live | + +**Оставшаяся дорожная карта:** + +| Приоритет | Фича | Влияние | +|----------|---------|--------| +| Высокий | Shared liquidity pools (category-level AMMs) | Решает фрагментацию ликвидности на уровне архитектуры | +| Высокий | Automated data oracles (экзогенные фиды) | Устраняет манипуляцию для объективных рынков | +| Средний | Тиры окон споров (малые vs крупные рынки) | Лучшая калибровка UX | +| Средний | LMSR batch settlement (расширить batch/commit-reveal на multi) | Multi-рынки сейчас форсят instant-ставки | +| — | Commit-reveal голосование в **спорах** | **Намеренно отклонено** — споры остаются публичными слушаниями (см. §4.2) | + +--- + +## 9. Что Onix НЕ заявляет + +Честное раскрытие компромиссов и ограничений: + +- **Прибыль LP не гарантирована.** Если у рынка ноль проигранных ставок, комиссий для распределения нет. + LP получает назад принципал, но не зарабатывает ничего. + +- **Платформенный риск существует.** Баги, эксплойты и атаки на управление отделены от модели + маркет-мейкера. Гарантия LP структурна (архитектура выплат), не застрахована (нет внешнего гарантийного + фонда). + +- **Токены Onix Multi — не инструменты фиксированной стоимости.** В стандартном LMSR 1 выигрышный токен = + 1 единица валюты. В Onix Multi токены — пропорциональные претензии на пул проигравших. Если все беттеры + выбрали победителя, все выходят в ноль. + +- **Доходность LP зависит от объёма, не от глубины.** Рынок с субсидией 100 000 VIZ и рынок с 1000 VIZ + заработают одинаковую абсолютную комиссию, если у обоих идентичные объём ставок и ставки комиссий. + Субсидия даёт глубину, не доходность. + +- **У DPoS-управления есть известные компромиссы.** Меньше валидаторов, чем у PoW/PoS, риски концентрации + делегатов, token-weighted голосование. Это присуще модели DPoS (общей с EOS, Hive, Tron), не специфично + для VIZ. + +- **Ликвидность токена VIZ сейчас низкая.** Экономические гарантии (страховые бонды, dispute fee) + масштабируются с ценой токена. Протокол предполагает, что полезность со временем создаёт спрос — та же + ставка, что делает любой protocol-native токен-проект. + +--- + +## 10. Заключение + +Onix Protocol адресует фундаментальный барьер внедрения прогнозных рынков: риск LP. Структурно отделяя +капитал LP от расчёта по ставкам — и в бинарных (CPMM), и в мульти-исходных (LMSR + parimutuel) рынках — +Onix делает предоставление ликвидности безрисковым и доступным для ритейл-участников. + +Ключевые инновации: + +1. **Гарантия принципала LP** как архитектурный инвариант, а не страховка +2. **Проигравшие-финансируют-победителей** расчёт, устраняющий конкуренцию комиссий с выплатами +3. **LMSR-ценообразование с parimutuel-расчётом** (Onix Multi) — сочетание проверенного price discovery + с безопасностью LP +4. **Time-weighted распределение LP**, награждающее раннюю фиксацию капитала +5. **Квадратичный time penalty** на прибыль (никогда принципал) для поздних ставок +6. **Lazy-пул ликвидности** с авто-аллокацией, graduated recall и MasterChef-учётом — также фондирующий + сабсистему **плеча** (маржа из пула, ликвидация по pre-bet резервам, нет bad-debt вне ограниченного + cancel-bet пути) +7. **Модель bonded-оракула** со скорингом репутации, offer→quote заморозкой комиссии и двухрежимной + системой споров — диспуты комитета это **публичные слушания** с изменяемыми, lazy-pool-взвешенными + голосами +8. **Опциональный анти-MEV** — batch / commit-reveal ставки (бинарные) с медианным kill-switch +9. **Реализация на уровне консенсуса** на VIZ DLT — без смарт-контрактов, газа и внешних keeper'ов; + строго **zero-sum** (протокол никогда не печатает токен) + +Ставка проста: если безрисковый LP привлекает капитал, капитал создаёт глубину, глубина улучшает цены, а +цены привлекают бетторов — то Onix Protocol решает проблему ликвидности прогнозных рынков. Механика +протокола математически проверяема. Экономическую гипотезу проверит рынок. + +--- + +## 11. Автор и раскрытие + +### Автор + +**Анатолий Пискунов** (On1x) — российский IT-инноватор, Web3/DLT-разработчик, создатель блокчейна VIZ. +Его работа охватывает distributed ledger technology, децентрализованные социальные протоколы и +экономические модели цифровых сообществ. + +Ключевые вклады: VIZ Blockchain (Fair DPoS, примитивы социального капитала), Onix Protocol (прогнозные +рынки с гарантией LP), Voice Protocol (цензуроустойчивый обмен сообщениями) и обширные публикации по +экономике блокчейна и Web3-архитектуре. + +Полный список публикаций и проектов: [https://on1x.com](https://on1x.com) + +### Раскрытие + +Автор Forecaster и Onix Protocol также создатель VIZ DLT. Дорожная карта миграции предлагает перенести +платформу на блокчейн, спроектированный и построенный автором. + +Это раскрыто заранее. Это также норма: Polymarket зависит от инфраструктуры Polygon Labs, Kalshi работает +на своих серверах, Augur спроектировал токен REP, на котором работает. Каждая платформа аргументирует за +свою инфраструктуру. Вопрос не в том, есть ли у автора интерес — он всегда есть — а в том, фальсифицируемы +ли технические заявления. Каждая формула, доказательство и механизм в этом документе математически +проверяемы, а кодовая база — открытая. + +--- + +## 12. Ссылки + +1. Hanson, R. (2003). *Combinatorial Information Market Design.* Information Systems Frontiers, 5(1), 107–119. — Logarithmic Market Scoring Rule (LMSR). + +2. Adams, H., Zinsmeister, N., Robinson, D. (2020). *Uniswap v2 Core.* — Constant Product Market Maker (`x * y = k`). + +3. Adams, H., et al. (2021). *Uniswap v3 Core.* — Концентрированная ликвидность и анализ impermanent loss. + +4. Gnosis. *Conditional Tokens Framework (CTF) Documentation.* https://docs.gnosis.io/conditionaltokens/ — ERC-1155 позиции прогнозного рынка. + +5. UMA Protocol. *Optimistic Oracle Documentation.* — Механизм эскалации споров, используемый Polymarket. + +6. Leshner, R., Hayes, G. (2019). *Compound: The Money Market Protocol.* — Паттерн аккумулятора cToken (основа для reward_per_share). + +7. SushiSwap. *MasterChef Contract.* — Паттерн ленивого учёта для распределения наград. + +8. Piskunov, A. (2019). *VIZ blockchain system: technical description.* — Архитектура VIZ DLT, DPoS-консенсус, именованные аккаунты. + +9. Piskunov, A. (2019). *What is Fair DPoS.* — Инновация управления в delegated proof of stake. + +10. Piskunov, A. (2023). *VIZ as a Digital Representative Self-Governing State.* — Рамка для блокчейн-систем как цифровых государств. diff --git a/@l10n/ru/docs/prediction-markets/workflows.md b/@l10n/ru/docs/prediction-markets/workflows.md new file mode 100644 index 0000000000..85b97c9440 --- /dev/null +++ b/@l10n/ru/docs/prediction-markets/workflows.md @@ -0,0 +1,741 @@ +--- +title: Прогнозные рынки — воркфлоу и диаграммы взаимодействия +description: Один канонический бинарный рынок Onix, прослеженный через каждого участника, с zero-sum мастер-леджером для нормального и спорного разрешения. +--- + +# Воркфлоу и диаграммы взаимодействия + +Один **канонический сценарий**, прослеженный через каждого участника. Каждая роль отправляет конкретные +**подписанные операции**, её затрагивают конкретные **виртуальные операции**, и всё завершается таблицей +**отправлено / получено токенов** для двух исходов: + +- **Нормальное разрешение** — оракул резолвит, проходит grace, `pm_auto_payout` рассчитывает. Без спора. +- **Спорное разрешение** — оракул резолвит **A**, спор **переворачивает на B**, затем идёт расчёт. + +Все суммы — абстрактные **VIZ**. Все проценты — **bp** (10000 = 100.00%). Расчёт строго **zero-sum** — +токены никогда не печатаются, `current_supply` не трогается: + +``` +Σ winner_payout + oracle_take + creator_take + lp_bonus + LP_principal + == Σ all bet amounts + LP_principal + forfeit_pool (+ insurance slash, в споре) +``` + +## Канонический рынок **M** (binary CPMM, A vs B) + +| Параметр | Значение | +|------|-------| +| Движок | binary CPMM (`x·y=k`), `weight = tokens_out` | +| Сид-ликвидность (marketmaker) | **2000** → резервы A=1000 / B=1000 | +| `oracle_fee_percent` (котировка оракула) | **1000** (10%) | +| `creator_fee_percent` | **500** (5%) | +| `liquidity_fee_percent` | **500** (5%) | +| `oracle_fixed_fee` (котировка оракула) | **10** | +| `dispute_penalty_percent` | **+10000** (слеш до 100% страховки ×consensus) | + +Иллюстративные chain props: `pm_market_creation_fee` 5, `pm_oracle_registration_fee` 10, +`pm_min_oracle_insurance` 5000, `pm_dispute_fee` 1000, `pm_dispute_reward_multiplier` 30000 (**3×**), +`pm_no_contest_penalty_percent` 5000, `pm_oracle_penalty_percent` 500, `pm_lazy_emergency_penalty_percent` +5000, `pm_leverage_pool_profit_percent` **R = 10%**, `pm_lazy_alloc_percent` 2000 (20%). + +### Состав + +| Актор | Роль | Стейк / действие | +|-------|------|----------------| +| **maker** | создатель + первый LP | сид 2000 ликвидности | +| **orac** | внешний оракул | страховка 5000; котирует 10% + фикс 10 | +| **LP1** | поставщик ликвидности в рынке | добавляет 1000 | +| **A** | беттор — победитель | 100 на **A**, рано; вес 100 | +| **C** | беттор — поздний победитель | 100 на **A** при T+85%; вес 100; time-penalty **50%** | +| **B** | беттор — проигравший | 200 на **B**; вес 200 | +| **D** | leverage **×10** победитель | collateral 10 + loan 90 (рынок **L**) | +| **E** | leverage **×5** ликвидирован | collateral 20 + loan 80 (рынок **L**) | +| **LZ1** | депозитчик lazy-пула | вносит 1000 | +| **disp** | диспутёр | escrow dispute fee 1000 | + +> Веса кривой (100 / 100 / 200) выписаны явно, чтобы арифметика parimutuel читалась; реальный CPMM выдаёт +> чуть меньше веса по мере сдвига резервов. + +## Диаграммы взаимодействия + +**Жизненный цикл рынка.** + +```mermaid +flowchart LR + W["Waiting (0)"] -->|оракул принял| A["Active (1)"] + W -->|оракул отклонил| X["Deleted (-1)"] + A -->|betting_expiration| C["Closed (2)"] + A -->|ранняя резолюция| R["Resolved (3)"] + C -->|оракул резолвит| R + R -->|grace, без спора| P["Выплачено"] + R -->|подан спор| D["Disputed"] + D -->|finalize / резолвер| P +``` + +**Расчёт — НОРМАЛЬНО (побеждает A).** Проигравшие финансируют победителей; принципал LP не тронут (zero-sum). + +```mermaid +flowchart TD + B["B проиграл 200 (пул проигравших)"] --> POOL{"делёж 200"} + POOL -->|oracle_fee 20 + fixed 10| OR["оракул +30"] + POOL -->|creator_fee 10| CR["создатель +10"] + POOL -->|liq_fee 10 + штраф 37| LPS["LP +47"] + POOL -->|winners_pool 150 → профит 75| A["A → выплата 175"] + POOL -->|профит 75 − time-penalty 37| C["C → выплата 138"] + MK["maker + LP1 принципал 3000"] -.возвращён полностью.-> MK +``` + +**Спор — оракул сказал A, перевёрнуто на B.** Наказание — это слеш страховки (отдельные деньги). + +```mermaid +sequenceDiagram + participant O as Оракул + participant D as Диспутёр + participant V as Комитет / Резолвер + O->>O: резолвит A + D->>V: pm_dispute_create (escrow dispute_fee) + O-->>V: обязательный ответ (дедлайн) + V->>V: pm_dispute_vote / pm_dispute_resolve → переворот на B + V-->>O: страховка слешнута (5000) + V-->>D: fee назад + награда (2000 из слэша) + V->>V: расчёт перезапускается → побеждает B +``` + +## Мастер-леджер — НОРМАЛЬНОЕ разрешение (A побеждает) + +`losers_sum = 200` (B). Комиссии с пула проигравших: +`oracle_fee = 200×10% = 20`, `creator_fee = 200×5% = 10`, `liq_fee = 200×5% = 10`, `oracle_fixed = 10`. +`winners_pool = 200 − 20 − 10 − 10 − 10 = 150`. `Σ выигрышного веса = 200` (A 100 + C 100). + +- **A**: профит `150×100/200 = 75`, штраф 0 → **выплата 175**. +- **C**: профит 75, time-penalty `75×50% = 37` (→ LP) → **выплата 138**. +- **LP-бонус** = `liq_fee 10 + штрафы 37 = 47`, делёж по времени в рынке: **maker ~31 / LP1 ~16**. +- **oracle_take** = `oracle_fee 20 + fixed 10 = 30`. **creator_take** = `creator_fee 10`. + +| Актор | отправил | получил | нетто (этот рынок) | +|-------|-------|----------|-------------------| +| maker | 2000 ликвидности + 5 creation-fee | 2000 принципал + 10 creator-fee + 31 LP-бонус | **+36** | +| orac | (10 reg-fee, 5000 страховка заблок.) | 30 oracle-take | **+30** | +| LP1 | 1000 ликвидности | 1000 принципал + 16 LP-бонус | **+16** | +| A | 100 | 175 | **+75** | +| C | 100 | 138 | **+38** | +| B | 200 | 0 | **−200** | + +**Zero-sum:** in `= ставки 400 + LP принципал 3000 = 3400`; out `= 175+138+0 + 30 + 10 + 47 + 3000 = 3400`. ✔ +5 creation-fee + 10 reg-fee уходят в **фонд DAO** (не часть пула рынка). + +## Мастер-леджер — СПОРНОЕ разрешение (оракул сказал A → перевёрнуто на B) + +`disp` эскроит `dispute_fee 1000`. Вердикт переворачивает на **B**; оракула слешат. +При `dispute_penalty_percent = 10000` и силе консенсуса **100%**: `slash = 5000×100%×100% = 5000`. +Carve-out награды: `reward_target = fee×3 = 3000` → `bonus = 3000 − 1000 = 2000` (≤ slash). Диспутёр +получает `fee 1000 + bonus 2000 = 3000`. Остаток `slash − bonus = 3000 → forfeit_pool`. + +Теперь **B побеждает**. `losers_sum = 200` (A 100 + C 100). Комиссии 20/10/10 + fixed 10. +`winners_pool = 200 − 50 + forfeit 3000 = 3150`. `Σ выигрышного веса = 200` (B). +- **B**: профит `3150×200/200 = 3150` → **выплата 3350**. +- **oracle_take** всё ещё `30` (*рыночная* комиссия платится из замороженной конфигурации даже при + перевороте — наказание это **слеш страховки**, отдельные деньги). **creator_take** 10. **LP-бонус** = liq 10. + +| Актор | отправил | получил | нетто (этот рынок) | +|-------|-------|----------|-------------------| +| maker | 2000 + 5 | 2000 принципал + 10 creator-fee + ~6 LP-бонус | **+11** | +| orac | страховка −**5000** слешнута | 30 oracle-take | **−4970** | +| LP1 | 1000 | 1000 принципал + ~4 LP-бонус | **+4** | +| A | 100 | 0 | **−100** | +| C | 100 | 0 | **−100** | +| B | 200 | 3350 | **+3150** | +| disp | 1000 dispute-fee | 3000 (fee назад + 2000 награда) | **+2000** | + +**Zero-sum:** in `= ставки 400 + LP принципал 3000 + dispute_fee 1000 + слеш 5000 = 9400`; +out `= B 3350 + оракул 30 + создатель 10 + lp_bonus 10 + LP принципал 3000 + диспутёр 3000 = 9400`. ✔ +Слеш 5000 делится на бонус диспутёра 2000 + forfeit 3000 (→ B через пул победителей). + +## Статус реализации (сверено с кодом) + +**Обычные операции — все 21 присутствуют** в variant `operation` (`operations.hpp`), валидируются + +оцениваются в `pm_evaluator.cpp`: +`pm_oracle_register`, `pm_oracle_update`, `pm_create_market`, `pm_oracle_accept_market`, `pm_place_bet`, +`pm_commit_bet`, `pm_reveal_bet`, `pm_cancel_bet`, `pm_add_liquidity`, `pm_withdraw_liquidity`, +`pm_resolve_market`, `pm_no_contest`, `pm_dispute_create`, `pm_dispute_vote`, `pm_dispute_resolve`, +`pm_transfer_position`, `pm_lazy_deposit`, `pm_lazy_withdraw`, `pm_leverage_open`, `pm_leverage_close`, +`pm_leverage_convert`. ✔ + +**Виртуальные операции** — эмитятся `database::process_pm_markets()` / эвалюаторами: + +| Виртуальная op | Фаерится? | Триггер (код) | +|------------|--------|----------------| +| `pm_market_accepted` | ✔ | при `pm_oracle_accept_market` **и** self-oracle `pm_create_market` | +| `pm_payout` | ✔ | **на каждую активную ставку** при расчёте — несёт `account`, `market_id`, `bet_id`, `side`/`outcome_index`, `amount` (стейк), `payout` (**0 при проигрыше**) | +| `pm_auto_payout` | ✔ | **раз на рынок** при расчёте — сводный маркер (`bets_sum`) рядом с per-bet `pm_payout` | +| `pm_commit_forfeit` | ✔ | нераскрытый commit после `reveal_deadline` | +| `pm_dispute_finalize` | ✔ | `voting_end_time` комитета | +| `pm_dispute_auto_close` | ✔ | `auto_close_time` (анти-фриз) | +| `pm_oracle_missed_penalty` | ✔ | оракул пропустил `result_expiration` | +| `pm_lazy_recall` | ✔ | шаг graduated recall простаивающей аллокации | +| `pm_batch_settle` | ✔ | граница эпохи | +| `pm_leverage_liquidate` | ✔ | mid-market ликвидация: reason **0** opposing-bet, **1** cancel-bet (`cascade_liquidate`) | +| `pm_leverage_resolve` | ✔ | **расчёт** leverage-позиции: несёт `market_id`, `outcome_index`, `won`, `pool_received`/`bettor_received`, `leverage` (= `total_bet/collateral`) | + +См. [API плагина](../plugins/prediction-market-api) для read-методов +(`get_account_leverage_positions`, `get_market_leverage_positions`, `get_creator_ban`, `get_dispute_votes`, …); +per-bettor результаты (`pm_payout`) и расчёты плеча (`pm_leverage_resolve`) видны и в `account_history`. + +## Роли в каноническом сценарии + +Каждый участник прослежен через рынок **M** (и leverage-подрынок **L**): его диаграмма взаимодействия, +**подписанные** операции, **виртуальные** операции, которые его касаются, леджер +**отправлено / получено** для обоих исходов и указатель на проверку в коде. Каждый леджер по роли — +срез двух мастер-леджеров выше. + +### Маркет-мейкер (создатель + первый LP) + +Мейкер создаёт рынок M, вносит **2000** ликвидности (становясь первым `pm_liquidity_object`) и +предлагает **потолок-оферту** оракула. Он **не** разрешает (это делает оракул). + +```mermaid +flowchart LR + maker -->|pm_create_market| M[(pm_market_object
status=0)] + maker -->|seed 2000| LP0[(pm_liquidity_object
provider=maker)] + M -. fee 5 .-> DAO[(committee_fund)] + orac -->|pm_oracle_accept_market| M2[(M status=1)] + M2 -. VIRTUAL .-> VA[[pm_market_accepted]] + M2 ==>|pm_auto_payout| RET[principal 2000 + creator_fee + LP bonus] + RET --> maker +``` + +- **Отправляет:** `pm_create_market` (задаёт `oracle_fee_percent`/`oracle_fixed_fee` как **потолок-оферту** + плюс свои `creator_fee_percent` 5% и `liquidity_fee_percent` 5%; платит `pm_market_creation_fee` 5 → DAO, + блокирует `liquidity` 2000); опц. `pm_add_liquidity` / `pm_withdraw_liquidity` (принципал-сейф, заблокирован + от `betting_expiration` до резолюции). +- **Касаются:** `pm_market_accepted` (оракул акцептует, либо self-oracle при создании); `pm_auto_payout` + (возвращает принципал LP + взвешенную по времени долю LP-бонуса). + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | 2000 ликвидности + 5 fee-создания (→DAO) | 2000 принципала + **creator_fee 10** + **LP-бонус ~31** | **+36** | +| спорный (→B) | 2000 + 5 | 2000 принципала + creator_fee 10 + LP-бонус ~6 | **+11** | + +Creator fee **всё равно платится** из замороженной конфигурации рынка при перевороте — спор наказывает +**оракула** (слеш страховки), а не мейкера. Принципал LP возвращается безусловно. + +- **Self-oracle:** `oracle == creator` → активен при создании, `pm_market_accepted` с `self_oracle=true`, + и мейкер дополнительно получает `oracle_take`. +- **Проверка:** `pm_create_market_evaluator`; LP через `settle_liquidity`; `committee_fund += pm_market_creation_fee`. + **Наблюдать:** `get_market`, `list_markets_by_creator`, `get_market_liquidity` (`earned_fee`), `get_market_meta`. + +### Оракул (регистрация → акцепт-котировка → резолюция) + +Внешний оракул **orac** вносит страховку, **котирует** свою fee при акцепте (≤ оферты мейкера и ≤ +`pm_max_oracle_fee_percent`) и разрешает. Его рыночная fee платится из пула проигравших; бонд под риском +только при пропуске дедлайна или проигранном споре. + +```mermaid +flowchart LR + orac -->|pm_oracle_register
insurance 5000| O[(pm_oracle_object)] + orac -. reg-fee 10 .-> DAO[(committee_fund)] + orac -->|pm_oracle_accept_market
quote fee 10% + fixed 10| M[(M status=1)] + M -. VIRTUAL .-> VA[[pm_market_accepted]] + orac -->|pm_resolve_market A| M3[(M status=3)] + M3 ==>|grace passes| VP[[pm_auto_payout]] + VP -->|oracle_take 30| orac +``` + +- **Отправляет:** `pm_oracle_register` (блокирует страховку 5000, платит reg-fee 10 → DAO, задаёт advisory + прайс-лист); `pm_oracle_accept_market` (**котирует** fee 10% + fixed 10, каждое ≤ оферты создателя и ≤ + медианного кэпа; замораживает на M); `pm_resolve_market` (задаёт `winning_outcome`, открывает grace); + опц. `pm_oracle_update` / `pm_no_contest`. Стоячий прайс-лист также может авто-акцептовать рынки live + при создании — см. документ операций оракула. +- **Касаются:** `pm_market_accepted`; `pm_auto_payout` (зачисляет `oracle_take`); `pm_oracle_missed_penalty` + (не разрешил → слеш `pm_oracle_penalty_percent` страховки → DAO, возврат всех ставок). + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | страховка 5000 (заблокир.) + reg-fee 10 (→DAO) | **oracle_take 30** = fee 20 + fixed 10 | **+30** | +| спорный (→B) | страховка −**5000 слеш** | oracle_take 30 | **−4970** | + +Даже при перевороте оракул сохраняет небольшую **рыночную fee** (замороженный конфиг); наказание — это +**слеш страховки**, разбиваемый на награду диспутёра и `forfeit_pool` победителей. Котировать **ниже** +оферты можно (цена = репутация); **выше** — отклоняется. + +- **Проверка:** `pm_oracle_register_evaluator`, `pm_oracle_accept_market_evaluator` (≤ оферты, ≤ кэпа, + заморозка), скан пропущенного дедлайна в `process_pm_markets`. **Наблюдать:** `get_oracle`, `list_oracles`, + `get_market` (замороженные условия). + +### Оракул — поддержан в споре (победитель спора) + +Оракул разрешил **A**; диспутёр оспорил, но вердикт **поддерживает** A. Оракул сохраняет рыночную fee +**и** забирает потерянный `dispute_fee`; страховка нетронута, `disputes_won++`. + +```mermaid +flowchart LR + orac -->|pm_resolve_market A| M[(market resolved A)] + disp -->|pm_dispute_create| D[(dispute)] + D ==>|uphold A| FIN[[pm_dispute_finalize / resolve]] + FIN -->|dispute_fee 1000| orac + FIN --> AUTO[[pm_auto_payout settles A]] + AUTO -->|oracle_take 30| orac +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| спор, поддержан | страховка 5000 (**не** слеш) | oracle_take 30 + **dispute_fee 1000** | **+1030** | +| нормальный (без спора) | страховка 5000 (заблокир.) | oracle_take 30 | **+30** | + +Оспаривание оборачивается против диспутёра и **платит оракулу**. Рынок «доброй воли» +(`dispute_penalty_percent < 0`) может даже выдать оракулу бонус к fee при смене исхода — признавая честную +ошибку. **Проверка:** ветка uphold в `pm_dispute_finalize` / `pm_dispute_resolve`. **Наблюдать:** +`get_oracle` (`disputes_won`), `get_dispute`. + +### Оракул — перевёрнут + слеш (проигравший спор) + +Оракул разрешил **A**; спор **переворачивает на B**, страховка **слешится**. Он всё ещё забирает +крошечную замороженную рыночную fee (fee и наказание — разные деньги), но теряет большую долю бонда и +репутацию. + +```mermaid +flowchart LR + orac -->|pm_resolve_market A| M[(resolved A)] + disp -->|pm_dispute_create proposed=B| D[(dispute)] + D ==>|overturn to B| FIN[[pm_dispute_finalize / resolve]] + FIN -->|slash 5000| INS[oracle.insurance ↓] + INS --> SPLIT[bonus 2000 → disputer
3000 → forfeit_pool → B] + FIN --> AUTO[[pm_auto_payout settles B]] + AUTO -->|oracle_take 30| orac +``` + +`slash = страховка 5000 × dispute_penalty_percent (100%) × consensus_strength (100%) = 5000`, +**перераспределяется**, не сжигается: `bonus 2000 →` диспутёр, `3000 → forfeit_pool →` новые победители (B). +Нетто **−4970** против **+30** без спора. Слеш масштабируется с **силой консенсуса** (`winning_rshares / +max_rshares`); `dispute_penalty_percent < 0` (добрая воля) → **нет** слеша. Слешнутый оракул часто ещё и +**забанен** (следующая роль). **Проверка:** ветка overturn `pm_dispute_finalize` / `pm_dispute_resolve`; +fee по-прежнему из `mkt.oracle_fee_percent`. **Наблюдать:** `get_oracle` (`total_insurance_slashed`, +`banned_until`), `get_dispute`. + +### Забаненный оракул (и забаненный создатель) + +Бан — это **статус**, а не перевод: `pm_oracle_object.banned_until` (а для создателей — +`pm_creator_ban_object`) блокирует актора от **новых** рынков, пока не пройдёт таймстамп. Обычно идёт вместе +со слешем переворота, но сам по себе токены не двигает. + +```mermaid +flowchart LR + resolver -->|pm_dispute_resolve ban_oracle| O[(pm_oracle_object
banned_until = T)] + orac -->|pm_create_market / accept| CHK{now < banned_until?} + CHK -->|yes| REJ[REJECTED: 'Oracle is banned'] + CHK -->|no, expired| OK[allowed again] + resolver -->|ban_creator| CB[(pm_creator_ban_object)] + maker -->|pm_create_market| CHK2{banned?} + CHK2 -->|yes| REJ2[REJECTED: 'Creator is banned'] +``` + +- **Кто ставит:** account-режим → `pm_dispute_resolve` (`ban_oracle`/`ban_creator` + `…_until`); + committee-режим → `pm_dispute_finalize` масштабирует бан по консенсусу при перевороте. `banned_until = + time_point_sec::maximum()` ⇒ **перманентный**. +- **Токены:** сам бан — **0** (чистый статус); сопутствующий слеш — это случай переворота выше. Страховка + остаётся заблокированной, возвратна после снятия бана и при отсутствии активных рынков. +- Баны переживают снапшоты и ключуются по аккаунту — повторная регистрация бан не стирает. **Проверка:** + `pm_create_market_evaluator` (`"Oracle is banned"` / `"Creator is banned"`). **Наблюдать:** `get_oracle` + (`banned_until`, `bans_received`), **`get_creator_ban(account)`**. + +### Беттор A — ранний победитель + +**A** ставит **100 на сторону A рано** (без штрафа за время) и выигрывает, когда M разрешается в A. +Выплата = ставка + взвешенная по весу доля пула победителей. + +```mermaid +flowchart LR + A -->|pm_place_bet side=A 100| BET[(pm_bet_object
weight 100)] + BET --> M[(market M
reserves shift)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|payout 175| A +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | 100 | **175** | **+75** | +| спорный (→B) | 100 | **0** | **−100** | + +`profit = winners_pool 150 × weight 100 / Σweight 200 = 75`; без штрафа → выплата `100 + 75`. Переворот +делает A **проигравшей** стороной. Выигрыши идут **только** из ставок проигравших (+ forfeit), никогда из +эмиссии. **Отправляет:** `pm_place_bet` (`side=0`, instant); опц. `pm_transfer_position` / `pm_cancel_bet`. +**Проверка:** `pm_place_bet_evaluator`, `settle_market`. **Наблюдать:** `get_account_positions` +(`expected_payout`), `get_market_weight_sums`; реализованный `pm_payout` в `account_history`. + +### Беттор B — проигравший + +**B** ставит **200 на сторону B**. Когда M разрешается в **A**, ставка B финансирует победителей, а B не +получает ничего. В спорном пути B становится победителем. + +```mermaid +flowchart LR + B -->|pm_place_bet side=B 200| BET[(pm_bet_object
status active)] + BET --> M[(market M)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|status=resolved, payout 0| BET +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | 200 | **0** | **−200** | +| спорный (→B) | 200 | **3350** | **+3150** | + +200 от B **и есть** `losers_sum` (платит 40 fee + 150 пул победителей + LP-бонус) — паримутюэль-правило +«проигравшие финансируют победителей». При перевороте B выигрывает, и forfeit оракула 3000 вливается в пул +B (`payout = 200 + 3150`). Проигравшая ставка тоже фиксируется через `pm_payout` с **payout=0**. +**Проверка:** ветка проигравшего в `settle_market`. **Наблюдать:** `get_account_positions`, +`get_market_bets`, `get_dispute`. + +### Беттор C — поздний победитель (штраф за время) + +**C** ставит **100 на сторону A**, но **поздно** (T+85% окна ставок), поэтому **штраф за время** урезает +*только прибыль* (не принципал). Тот же вес, что у A, но получает меньше; урезанное идёт LP. + +```mermaid +flowchart LR + C -->|pm_place_bet side=A 100
at T+85%| BET[(pm_bet_object
weight 100
time_penalty 50%)] + BET --> M[(market M)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|payout 138| C + VP -. penalty 37 .-> LPb[LP bonus] +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | 100 | **138** | **+38** | +| спорный (→B) | 100 | **0** | **−100** | + +`profit = 75`; `penalty = 75 × 50% = 37` (→ LP); `payout = 100 + 75 − 37 = 138` — **−37** против +75 у A +при том же весе. Узел штампует `time_penalty` при размещении из штрафной кривой рынка +(`time_penalty_type/value`, `penalty_curve_type`). Это сдерживает снайпинг в последнюю секунду и +субсидирует ликвидность, а не протокол. **Проверка:** `pm_place_bet_evaluator` (eval кривой), +`compute_settlement`. **Наблюдать:** `get_account_positions` (`time_penalty`), `get_market_bets`. + +### Беттор D — leverage ×10 победитель + +**D** открывает позицию **×10**: **10 коллатерала + 90 займа** из lazy-пула = **100** на стороне A, в +изолированном leverage-рынке **L** (`pm_leverage_enabled=true`, `R = 10%`). Когда A выигрывает, D +сохраняет апсайд на всей сотне после погашения займа + процентов. +`pool_profit = loan 90 × R 10% = 9`; `obligation = 90 × 1.10 = 99`. + +```mermaid +flowchart LR + D -->|pm_leverage_open
collateral 10 + loan 90| POS[(pm_leverage_position
total_bet 100, obligation 99)] + POOL[(lazy pool)] -.loan 90.-> POS + POS --> L[(market L, side A)] + L ==>|settle: force_close at cancel_value| VR[[pm_leverage_resolve won=true, leverage=10]] + VR -->|min(cv,obligation) 99| POOL + VR -->|cv 200 − 99 = 101| D +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | коллатерал **10** | cancel_value 200 − obligation 99 = **101** | **+91** | +| спорный (→B) | коллатерал 10 | 0 | **−10** | + +Прибыльная позиция закрывается по `cancel_value`; пул забирает `obligation 99` (заём 90 + **9 процентов**), +D оставляет остаток на своих 10 → **+91** (пул **+9**). Leverage рассчитывается ликвидацией, **никогда** не +через `pm_auto_payout`. Zero-sum (L): in `10 + 90 + 100 = 200`; out `101 + 99 = 200`. **Отправляет:** +`pm_leverage_open`; опц. `pm_leverage_close` (только при `cv ≥ obligation`) / `pm_leverage_convert`. +**Касается:** `pm_leverage_resolve` (force-close при расчёте, `reason=expiration`). **Проверка:** +`force_close_positions` → `liquidate_position(reason=2)`. **Наблюдать:** +**`get_account_leverage_positions`** / **`get_market_leverage_positions`**, `get_lazy_pool`. + +### Беттор E — leverage ×5 ликвидирован + +**E** открывает позицию **×5**: **20 коллатерала + 80 займа** = **100** на стороне B (рынок **L**, +`R = 10%`). До резолюции **встречная ставка** двигает кривую против B; **каскадная ликвидация** принудительно +закрывает позицию. **E теряет коллатерал, но пул всегда остаётся целым.** `obligation = 80 × 1.10 = 88`. + +```mermaid +flowchart LR + E -->|pm_leverage_open
collateral 20 + loan 80| POS[(pm_leverage_position
obligation 88)] + POOL[(lazy pool)] -.loan 80.-> POS + X -->|pm_place_bet side=A| L[(market L)] + L ==>|cascade at PRE-bet reserves
cv 88 ≤ threshold| VL[[pm_leverage_liquidate
reason=opposing_bet]] + VL -->|pool_received 88 = loan 80 + profit 8| POOL + VL -->|bettor_received 0| E +``` + +E ликвидируется **до** резолюции, поэтому финальный результат A/B (спорный или нет) на неё не влияет: + +| актор | отправляет | получает | нетто | +|-------|------------|----------|-------| +| **E** | коллатерал **20** | **0** | **−20** | +| **пул** | заём 80 | **88** (заём 80 + R% 8) | **+8** | + +Ликвидации по встречной ставке идут на **до-ставочных** резервах, где `cancel_value ≥ loan`, поэтому +`pool_received = min(cv, obligation)` возвращает как минимум заём — пул **никогда** не теряет. + +> **Единственный путь в минус** — same-side **`pm_cancel_bet` (Case B)**: отмена разворачивает *прежнюю, +> крупную* ставку той же стороны (за пределами per-bet слиппедж-кэпа) и ради честности к отменителю +> исполняется **первой** по его цене — так каскад может оказаться `cancel_value < loan`: +> `shortfall = obligation − cancel_value`, `lazy_pool.free_balance −= shortfall`. Этот **bad debt** +> **ограничен** (`≤ cancel_value_before × SL%`) и **редок** (R% пула со всех прочих позиций его перекрывает). +> Покрыт тестом `leverage_cancel_bet_cascade_bad_debt`. + +Защита пула структурна (`max_per_position`, `max_position_ratio`, `safety_margin`, слиппедж-кэп, +`expiration_buffer`). `pm_leverage_enabled=false` блокирует **только новые** открытия — каскад ликвидации +**не** гейтится флагом, поэтому управление не может снять защиту пула на лету +(`leverage_disabled_keeps_liquidation_protection`). **Проверка:** `pm_place_bet` → +`cascade_liquidate(reason=0)`; `pm_cancel_bet` → `cascade_liquidate(reason=1)`; `liquidate_position`. +**Наблюдать:** **`get_account_leverage_positions`** (`status=1`, `pool_received`, `bettor_received`), +`get_lazy_pool`. + +### Поставщик ликвидности в рынке (LP1) + +**LP1** добавляет **1000** ликвидности в активный M (после сида мейкера). Принципал **всегда** возвращается; +сверху он зарабатывает **взвешенную по времени** долю LP-бонуса (liquidity fee + штрафы за время + пыль). +Отличается от поставщика lazy-пула, который депонирует один раз и авто-аллоцируется по многим рынкам. + +```mermaid +flowchart LR + LP1 -->|pm_add_liquidity 1000| L1[(pm_liquidity_object
provider=LP1)] + L1 --> M[(market M reserves)] + M ==>|settle| SL[[settle_liquidity]] + SL -->|principal 1000 + bonus ~16| LP1 + LP1 -->|pm_withdraw_liquidity
after resolution| OUT[principal-safe exit] +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| нормальный (A) | 1000 | **1000 принципала + ~16 бонуса** | **+16** | +| спорный (→B) | 1000 | 1000 принципала + ~4 бонуса | **+4** | + +Пул LP-бонуса = `liq_fee 10 + штрафы 37 = 47`, делится по `principal × секунды-в-рынке` (ранний мейкер ~31, +поздний LP1 ~16). **Гарантия принципала** архитектурна — сид возвращается до выплаты любому победителю; LP +может лишь недополучить бонус, но не потерять принципал. Вывод заблокирован от `betting_expiration` до +резолюции. **Проверка:** `pm_add_liquidity_evaluator` (фиксирует `deposit_time`), `settle_liquidity` → +`distribute_lp`. **Наблюдать:** `get_market_liquidity` (`earned_fee`), `get_market_weight_sums`. + +### Lazy-пул ликвидности (системный объект) + +**Синглтон** `pm_lazy_pool_object` — не аккаунт. Депозитчики финансируют его один раз; пул +**авто-аллоцирует** долю в каждый акцептованный рынок как молчаливый LP (`pm_liquidity_object` с пустым +`provider`), **финансирует leverage-займы** и **отзывает** простаивающие аллокации. Зарабатывает LP-доход + +проценты по плечу, учёт в стиле MasterChef (один глобальный `reward_per_share`, O(1) — см. белую бумагу). +Поля: `total_shares`, `free_balance`, `allocated_balance`, `earned_balance`, `reward_per_share`, +`leverage_fund_used`. + +```mermaid +flowchart TD + LZ1 -->|pm_lazy_deposit 1000| POOL[(pm_lazy_pool
free 1000 / shares 1000)] + POOL ==>|on market accept
alloc 20% = 200| ALLOC[(pm_lazy_allocation
+ pm_liquidity provider=∅)] + ALLOC -->|market settles| YLD[route_pool_lp_return
principal 200 + yield 20] + YLD --> POOL + POOL -->|leverage loan 90| Dpos[D position] + Dpos -->|close/resolve: 90 + interest 9| POOL + POOL -. idle market .-> VR[[pm_lazy_recall]] + VR -->|step back to free| POOL +``` + +| денежный поток пула | эффект | +|---------------------|--------| +| `pm_lazy_deposit` | `free_balance += amount`, минт shares | +| авто-аллокация (при акцепте) | `free → allocated` (молчаливый LP) | +| рынок рассчитывается | `route_pool_lp_return`: принципал + доход → `free`; доход → `earned` & `reward_per_share` | +| leverage open (D/E) | `free −= loan`, `leverage_fund_used += loan` | +| leverage close / resolve / ликвидация по встречной ставке | `min(cv, obligation) → free`; `cv ≥ loan` ⇒ **никогда не убыток** | +| ликвидация cancel-bet (только Case B) | возвращает `cv`, который **может быть < loan** → ограниченный **bad debt** | +| `pm_lazy_recall` (простой рынок) | один шаг 10% простаивающей аллокации → `free` | +| `pm_lazy_withdraw` | сжечь shares → принципал + pending; emergency-штраф остаётся в пуле | + +За канонический сценарий пул в нетто **+37 earned** (доход рынка M +20, проценты leverage D +9, возврат по +встречной ставке leverage E +8). Как рыночный LP принципал возвращается безусловно; с диспутом меняется лишь +*бонусный* доход. + +> Пул выполняет **обе** роли из единого `free_balance`: рыночные LP-аллокации (`maybe_allocate_lazy`) и +> leverage-займы (`leverage_fund_used` ограничивает последние). Все leverage-параметры проверяются **в момент +> `pm_leverage_open`** против текущей медианы, поэтому позднейшие изменения свойств влияют лишь на *новые* +> открытия, а не на уже выданные займы. + +VIZ в пуле **ликвидны**, не vested → **нет** веса для планирования валидаторов или committee-request. +**Исключение (HF14):** для **PM-споров комитета** стейк депозитчика в пуле **учитывается** — конвертируется в +vesting-shares через `get_vesting_share_price()` и добавляется к весу его `pm_dispute_vote` (см. резолвер- +комитет ниже). **Проверка:** `apply_hardfork(CHAIN_HARDFORK_14)` (синглтон), `maybe_allocate_lazy`, +`route_pool_lp_return`. **Наблюдать:** `get_lazy_pool`. + +### Поставщик ликвидности в lazy-пуле (LZ1) + +**LZ1** депонирует **1000** в пул **один раз** и даёт ему распределить по рынкам + leverage-займам. Он +зарабатывает долю агрегированного дохода пула (`reward_per_share`), а не исход одного рынка. Два выхода: +**плановый** (после лока) и **аварийный** (до лока, со штрафом на *прибыль*). + +```mermaid +flowchart LR + LZ1 -->|pm_lazy_deposit 1000| DEP[(pm_lazy_deposit_object
shares 1000, unlock=+7d)] + DEP --> POOL[(lazy pool)] + POOL -. yield accrues .-> RPS[reward_per_share ↑] + LZ1 -->|pm_lazy_withdraw| OUT{planned or
emergency?} + OUT -->|planned, t≥unlock| P[principal 1000 + pending 29] + OUT -->|emergency, t|pm_dispute_create proposed=B
escrow fee 1000| D[(pm_dispute_object
status open)] + D --> VOTE{committee vote
or account resolve} + VOTE ==>|overturn to B| FIN[[pm_dispute_finalize / resolve]] + FIN -->|fee 1000 + bonus 2000| disp + FIN -.slash 5000 from oracle.-> SPLIT[bonus 2000 → disp
3000 → forfeit_pool → winners] +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| спор, перевёрнут («победа») | dispute_fee **1000** | fee 1000 назад + **bonus 2000** | **+2000** | + +`reward_target = fee × pm_dispute_reward_multiplier (3×) = 3000` → `bonus = 3000 − 1000 = 2000`, **ограничен +фактическим слешем**; остаток (3000) → `forfeit_pool` → новые победители. disp рисковал 1000, уходит +**+2000**. (Committee-режим: disp **не** голосует сам — это делает электорат SHARES.) **Проверка:** +`pm_dispute_create_evaluator`, ветка overturn `pm_dispute_finalize`/`pm_dispute_resolve`. **Наблюдать:** +`get_dispute`, `get_dispute_votes`. + +### Диспутёр — fee потеряна (оракул поддержан) + +**disp** оспаривает **A** оракула, но вердикт **поддерживает оракула**. Эскроу-fee **переходит оракулу** как +компенсация, а рынок рассчитывается как изначально (A выигрывает). + +```mermaid +flowchart LR + disp -->|pm_dispute_create proposed=B
escrow fee 1000| D[(pm_dispute_object)] + D --> VOTE{committee vote
or account resolve} + VOTE ==>|uphold oracle A| FIN[[pm_dispute_finalize / resolve]] + FIN -->|dispute_fee 1000| orac[oracle compensation] + FIN -->|market settles as A| AUTO[[pm_auto_payout]] +``` + +| исход | отправляет | получает | нетто | +|-------|------------|----------|-------| +| спор, поддержан («проигрыш») | dispute_fee **1000** | **0** | **−1000** | + +Fee — это skin-in-the-game диспутёра: неверный/легкомысленный спор платит оракулу. Эта асимметрия (потерять +fee при ошибке, выиграть кратное при правоте) держит канал честным. Спор, который так и **не решён** (оракул +молчит / нет кворума), принудительно закрывается, а fee **возвращается** (нетто 0) — см. авто-закрытие спора +ниже, что отличается от проигрыша по существу. **Проверка:** ветка uphold +`pm_dispute_finalize`/`pm_dispute_resolve`. **Наблюдать:** `get_dispute`, `get_oracle` (получает fee, +`disputes_won++`). + +### Резолвер — комитет (взвешенный по стейку, dispute_mode = 0) + +*Весь электорат SHARES* решает **взвешенным по стейку голосованием**; единого резолвер-аккаунта нет. Вердикт +детерминированно подсчитывается `pm_dispute_finalize` в `voting_end_time`. + +**Вес голоса** = живые **`effective_vesting_shares`** (`vesting − delegated + received`) **плюс стейк +lazy-пула, сконвертированный в vesting-shares**, поскольку многие члены DAO держат VIZ в пуле (где они +ликвидны): + +``` +pool_claim_viz = pool_NAV × deposit.shares / pool.total_shares +pool_weight = pool_claim_viz × get_vesting_share_price() +voter_weight = effective_vesting_shares + pool_weight +``` + +Знаменатель кворума участия — `total_vesting_shares + (pool_NAV → vesting-shares)`. 7-дневный лок депозита +предотвращает игру депозит-голос-вывод. + +```mermaid +flowchart LR + V1[voter · eff_vshares] -->|pm_dispute_vote outcome,percent| D[(pm_dispute_votes)] + V2[voter · eff_vshares] -->|pm_dispute_vote| D + D ==>|voting_end_time| FIN[[pm_dispute_finalize]] + FIN -->|argmax rshares,
threshold check| VERDICT{uphold / overturn} + VERDICT -->|consensus_strength
scales slash & bans| OUT[settle] +``` + +- **Отправляют (голосующие):** `pm_dispute_vote` — **auth `regular`**. `vote_outcome = -1` поддерживает, + иначе предлагает верный исход; `vote_percent ∈ [-10000, 10000]`. Голосующий может **пересматривать** + бюллетень сколько угодно раз, пока голосование открыто — повторный голос **перезаписывает** прежний + (побеждает последний, без «Already voted»). +- **Без commit-reveal — намеренно, меняться НЕ будет.** Спор комитета — **открытое публичное слушание**: + текущий подсчёт виден (`get_dispute_votes`), голоса не скрыты. Ценность DAO — разрешать споры максимально + правдиво и прозрачно; новые аргументы всплывают в ходе голосования, и голосующие *должны* обновляться; а + голосующим **не платят** за совпадение с большинством, поэтому обычный анти-стадный довод (beauty contest) + для commit-reveal здесь неприменим. + +| актор | отправляет | получает | +|-------|------------|----------| +| каждый голосующий | 0 | **0** — голосование это управленческий долг, не оплачиваемое действие | + +Голосующие никогда не получают токены; влияние — чистый вес стейка. Экономические потоки приходятся на +диспутёра, оракула и бетторов согласно спорному мастер-леджеру выше. Нишевые рынки могут не пройти порог → +переход к авто-закрытию спора ниже. **Проверка:** `pm_dispute_vote_evaluator` (modify-or-create на +`by_market_voter`); `pm_dispute_finalize` (`lazy_vote_weight`, `get_vesting_share_price`, кворум, argmax, +`consensus_strength`). **Наблюдать:** `get_dispute_votes` (живой подсчёт + проекция finalize: +`quorum_percent_bp`, `expected_uphold`, `expected_outcome`, `expected_consensus_strength_bp`). Тесты: +`committee_dispute_lazy_pool_voting_weight`, `committee_dispute_flips_outcome`. + +### Резолвер — один аккаунт (централизованный, dispute_mode = 1) + +Рынок называет один аккаунт `dispute_resolver` (например, мультисиг регулятора), который решает в одиночку — +**без веса стейка, без голосования DAO**. Задан при создании, должен отличаться и от `oracle`, и от +`creator` (анти-самосуд). Тот же набор операций, что и в комитете; различается лишь *кто решает*. + +```mermaid +flowchart LR + disp -->|pm_dispute_create| D[(dispute, mode=1)] + resolver -->|pm_dispute_resolve
correct_outcome=B
penalty_amount, ban flags| FIN[[verdict]] + FIN -->|slash penalty_amount| orac[oracle.insurance ↓] + FIN -->|fee + reward| disp + FIN --> AUTO[[pm_auto_payout settles B]] +``` + +- **Отправляет:** `pm_dispute_resolve` — **auth `active` названного `dispute_resolver`** только: + `correct_outcome`, `penalty_amount` (страховка к слешу — фиксированная сумма, **не** масштабируется + стейком), `ban_oracle`/`ban_creator` (+ `…_until`). + +| актор | отправляет | получает | +|-------|------------|----------| +| резолвер | 0 | **0** — нейтральный арбитр | + +Пост-вердиктный канон идентичен комитет-режиму; различается лишь размер слеша (заданный резолвером +`penalty_amount`, без масштабирования `consensus_strength`, поскольку решает один). KYC/whitelisting +резолвера — забота **клиентского слоя**. **Проверка:** `pm_dispute_resolve_evaluator` (только названный +резолвер, `dispute_mode==1`). **Наблюдать:** `get_dispute`, `get_oracle`, **`get_creator_ban(account)`**. + +### Спор принудительно завершён (анти-фриз авто-закрытие) + +Спор, который так и **не решён** — оракул молчит и (в комитете) нет кворума — не может заморозить рынок +навсегда. В `auto_close_time` обработчик `pm_dispute_auto_close` принудительно его завершает: **всем +возврат**, fee диспутёра **возвращается**, неотзывчивый оракул наказывается. Победитель не выбирается. + +```mermaid +flowchart LR + disp -->|pm_dispute_create
escrow fee 1000| D[(dispute, status open)] + D -. oracle silent / no quorum .-> WAIT[auto_close_time reached] + WAIT ==>|VIRTUAL| AC[[pm_dispute_auto_close]] + AC -->|refund all bets| bettors + AC -->|fee 1000 back| disp + AC -->|insurance slash → DAO| orac +``` + +| актор | отправляет | получает | нетто | +|-------|------------|----------|-------| +| A / B / C | ставка | полный возврат | **0** | +| maker / LP1 | ликвидность | принципал назад | **0** (без бонуса) | +| disp | dispute_fee 1000 | **1000 назад** | **0** | +| orac | страховка −слеш → DAO | — | **− слеш** | + +Это **не** «диспутёр проиграл»: возвращённая fee (нетто 0) отличается от потерянной fee (диспутёр-проигравший, +нетто −1000). Никто не зарабатывает; рынок аннулируется, чтобы снять заморозку, издержки падают на не +ответившего оракула. Та же форма аннуляции-и-возврата покрывает `pm_oracle_missed_penalty` и `pm_no_contest`. +Настройте `pm_dispute_auto_close_sec` (14 д) против `pm_dispute_vote_period_sec` (3 д), чтобы честные споры +решались раньше. **Проверка:** скан авто-закрытия в `process_pm_markets` (`refund_all_bets` + +`return_liquidity` + зачёт fee; `disputes_auto_closed++`). **Наблюдать:** `get_dispute` (статус → +авто-закрыт), `get_market`, `get_oracle`. diff --git a/@l10n/ru/docs/protocol/operations/overview.md b/@l10n/ru/docs/protocol/operations/overview.md index e2dcdb3200..dcfc0a81ab 100644 --- a/@l10n/ru/docs/protocol/operations/overview.md +++ b/@l10n/ru/docs/protocol/operations/overview.md @@ -52,6 +52,32 @@ | 58 | `use_invite_balance_operation` | active | [Инвайты](./invites.md) | | 60 | `fixed_award_operation` | regular | [Награды](./awards.md) | | 61 | `target_account_sale_operation` | master | [Рынок аккаунтов](./account-market.md) | +| 64 | `set_reward_sharing_operation` | active | [Валидаторы](./validators.md) | +| 66 | `pm_oracle_register_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 67 | `pm_oracle_update_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 68 | `pm_create_market_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 69 | `pm_oracle_accept_market_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 70 | `pm_place_bet_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 71 | `pm_commit_bet_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 72 | `pm_reveal_bet_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 73 | `pm_cancel_bet_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 74 | `pm_add_liquidity_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 75 | `pm_withdraw_liquidity_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 76 | `pm_resolve_market_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 77 | `pm_no_contest_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 78 | `pm_dispute_create_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 79 | `pm_dispute_vote_operation` | regular | [Прогнозные рынки](./prediction-markets.md) | +| 80 | `pm_dispute_resolve_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 81 | `pm_transfer_position_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 82 | `pm_lazy_deposit_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 83 | `pm_lazy_withdraw_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 91 | `pm_leverage_open_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 92 | `pm_leverage_close_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 93 | `pm_leverage_convert_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 98 | `pm_dispute_oracle_respond_operation` | active | [Прогнозные рынки](./prediction-markets.md) | +| 99 | `pm_unban_operation` | active | [Прогнозные рынки](./prediction-markets.md) | + +> ID — это фиксированный индекс в едином `operation`-варианте цепи (только добавление). Пропуски в этой таблице — **виртуальные** операции (ниже), чередующиеся по ID — например, 62–63, 65, 84–90, 94–97, 100. --- @@ -83,6 +109,19 @@ | 59 | `expire_escrow_ratification_operation` | Истёк дедлайн эскроу | [Виртуальные операции](../virtual-operations.md) | | 62 | `bid_operation` | Сделана ставка на аукционе | [Виртуальные операции](../virtual-operations.md) | | 63 | `outbid_operation` | Перебитая ставка на аукционе | [Виртуальные операции](../virtual-operations.md) | +| 65 | `stakeholder_reward_operation` | Выплата reward-sharing участнику | [Валидаторы](./validators.md) | +| 84 | `pm_batch_settle_operation` | Рассчитана граница batch-эпохи | [Прогнозные рынки](./prediction-markets.md) | +| 85 | `pm_commit_forfeit_operation` | Escrow commit-reveal форфейтнут (не раскрыт) | [Прогнозные рынки](./prediction-markets.md) | +| 86 | `pm_auto_payout_operation` | Рынок рассчитан (порыночный маркер выплаты) | [Прогнозные рынки](./prediction-markets.md) | +| 87 | `pm_dispute_finalize_operation` | Подсчитано голосование комитета | [Прогнозные рынки](./prediction-markets.md) | +| 88 | `pm_dispute_auto_close_operation` | Анти-фриз авто-закрытие спора | [Прогнозные рынки](./prediction-markets.md) | +| 89 | `pm_oracle_missed_penalty_operation` | Оракул пропустил дедлайн разрешения | [Прогнозные рынки](./prediction-markets.md) | +| 90 | `pm_lazy_recall_operation` | Шаг поэтапного отзыва lazy-пула | [Прогнозные рынки](./prediction-markets.md) | +| 94 | `pm_leverage_liquidate_operation` | Плечевая позиция ликвидирована | [Прогнозные рынки](./prediction-markets.md) | +| 95 | `pm_leverage_resolve_operation` | Плечевая позиция рассчитана на резолюции | [Прогнозные рынки](./prediction-markets.md) | +| 96 | `pm_market_accepted_operation` | Рынок запущен (оракул принял / self / авто) | [Прогнозные рынки](./prediction-markets.md) | +| 97 | `pm_payout_operation` | Паримутюэль-выплата на беттера | [Прогнозные рынки](./prediction-markets.md) | +| 100 | `pm_ban_expired_operation` | Истёк временный бан оракула/создателя | [Прогнозные рынки](./prediction-markets.md) | --- diff --git a/@l10n/ru/docs/protocol/operations/prediction-markets.md b/@l10n/ru/docs/protocol/operations/prediction-markets.md index 8d3ca48bc2..0210f0b1d7 100644 --- a/@l10n/ru/docs/protocol/operations/prediction-markets.md +++ b/@l10n/ru/docs/protocol/operations/prediction-markets.md @@ -106,6 +106,8 @@ flowchart TD Оракул принимает (`status → active`) или отклоняет (ликвидность возвращается создателю; `status → deleted`) ожидающий рынок. При принятии оракул **котирует свои фактические условия** через `oracle_fee_percent` + `oracle_fixed_fee` — каждое должно быть `≤` оферты создателя на рынке, а `oracle_fee_percent ≤ pm_max_oracle_fee_percent`. Котировка **замораживается в рынок**, эмитится виртуальная `pm_market_accepted` (чтобы парсеры истории видели запуск + условия). Расчёт позже читает только эти замороженные поля — никогда живую медиану. +Оракул обязан действовать в течение `pm_oracle_accept_window_sec` (по умолчанию 1 ч) от создания. Если он не сделал ни того, ни другого к `accept_deadline` рынка, крон каждого блока аннулирует рынок (`status → deleted`), возвращает создателю сид-ликвидность (**не** невозвратную комиссию за создание) и эмитит `pm_market_expired` (см. Виртуальные операции). + | Поле | Тип | Описание | |------|-----|----------| | `market_id` | `int64` | Ожидающий рынок | @@ -155,7 +157,14 @@ Commit-reveal фаза 2: раскрывает ставку и ставит её ### `pm_resolve_market_operation` (ID 76) **Auth:** `active` аккаунта `oracle` -Оракул разрешает в `winning_outcome`. Открывает окно ожидания спора (`result_expiration + pm_dispute_grace_sec`); по его истечении `pm_auto_payout` рассчитывает. +Оракул разрешает в `winning_outcome`. Открывает окно ожидания спора (`result_expiration + pm_dispute_grace_sec`); по его истечении `pm_auto_payout` рассчитывает. Заявление оракула о разрешении **сохраняется на рынке** (как `rules_url` оракула), чтобы клиент мог прочитать его прямо через `get_market`, не сканируя историю. + +| Поле | Тип | Описание | +|------|-----|----------| +| `market_id` | `int64` | Целевой рынок | +| `winning_outcome` | `int16_t` | Индекс выигравшего исхода | +| `decision_url` | `string` | Ссылка на доказательства, `≤ MAX_PM_DECISION_URL_LEN`; сохраняется на рынке | +| `decision_reason` | `string` | Свободное обоснование, `≤ MAX_PM_DISPUTE_REASON_LEN`; сохраняется на рынке (`decision_reason`) | ### `pm_no_contest_operation` (ID 77) **Auth:** `active` аккаунта `oracle` @@ -177,7 +186,39 @@ Commit-reveal фаза 2: раскрывает ставку и ставит её ### `pm_dispute_resolve_operation` (ID 80) **Auth:** `active` аккаунта `resolver` -Вердикт в режиме аккаунта от заданного рынком `dispute_resolver`. Может слешить `penalty_amount` страховки и банить оракула/создателя до заданных времён. +Вердикт в режиме аккаунта от заданного рынком `dispute_resolver`. Может слешить `penalty_amount` страховки и банить оракула/создателя до заданных времён (`ban_*_until = time_point_sec::maximum()` = навсегда). + +> **Баны — это фича комплаенса/регулятора, эксклюзивная для режима аккаунта.** Когда рынок направляет свои споры на `dispute_resolver` режима аккаунта (например, регулятора или лицензированного арбитра), этот резолвер может санкционировать **и оракула, и создателя рынка** — временно или навсегда — тем же вердиктом, поверх слэша страховки: это позволяет регулятору-резолверу отстранить недобросовестного оракула или создателя-рецидивиста от платформы. **Режим комитета/DAO (`dispute_mode == 0`) не имеет права бана по замыслу** — это прозрачные публичные слушания, которые лишь слешат страховку и корректируют репутацию (`pm_dispute_finalize`), но никогда не банят. Установленный здесь бан записывает выдавшего `resolver` в `banned_by` цели, так что снять его досрочно через `pm_unban` может только этот резолвер; иначе бан истекает на `banned_until` (крон эмитит `pm_ban_expired`). + +| Поле | Тип | Описание | +|------|-----|----------| +| `market_id` | `int64` | Оспариваемый рынок | +| `correct_outcome` | `int16_t` | Финальный верный исход (`-1` = void/no-contest) | +| `penalty_amount` | `asset` (VIZ) | Страховка оракула к слэшу | +| `ban_oracle` / `ban_oracle_until` | `bool` / `time_point_sec` | Забанить оракула до заданного времени | +| `ban_creator` / `ban_creator_until` | `bool` / `time_point_sec` | Запретить создателю создавать рынки до заданного времени | + +### `pm_dispute_oracle_respond_operation` (ID 98) +**Auth:** `active` аккаунта `oracle` + +Оракул рынка публикует **публичное опровержение** на открытый спор. Поскольку спор — это публичные слушания, текст сохраняется на объекте спора (`oracle_response` / `oracle_response_time`, читается через `get_dispute`), чтобы каждый голосующий/резолвер мог его учесть. Разрешено только пока спор открыт и `now ≤ oracle_response_deadline`; повторная публикация перезаписывает предыдущий ответ. + +| Поле | Тип | Описание | +|------|-----|----------| +| `market_id` | `int64` | Оспариваемый рынок | +| `response` | `string` | Текст опровержения, непустой, `≤ MAX_PM_DISPUTE_REASON_LEN` | + +### `pm_unban_operation` (ID 99) +**Auth:** `active` аккаунта `resolver` + +Снимает бан, наложенный `pm_dispute_resolve` режима аккаунта, **досрочно**. Снять его может только аккаунт, записанный в `banned_by` цели (резолвер, установивший бан); хотя бы один из `unban_oracle` / `unban_creator` должен быть задан, а соответствующий бан — быть активным сейчас. Ставит `banned_until` в прошлое и очищает `banned_by`. (Баны, не снятые здесь, просто истекают на `banned_until` — тогда крон эмитит `pm_ban_expired`.) + +| Поле | Тип | Описание | +|------|-----|----------| +| `resolver` | `account_name_type` | Аккаунт, наложивший бан (должен равняться `banned_by` цели) | +| `target` | `account_name_type` | Забаненный оракул / создатель | +| `unban_oracle` | `bool` | Снять бан оракула (`pm_oracle_object.banned_until`) | +| `unban_creator` | `bool` | Снять бан создателя (`pm_creator_ban_object.banned_until`) | ### `pm_transfer_position_operation` (ID 81) **Auth:** `active` аккаунта `from` @@ -213,6 +254,12 @@ Commit-reveal фаза 2: раскрывает ставку и ставит её | 95 | `pm_leverage_resolve_operation` | Расчёт — плечевая позиция принудительно закрыта: `outcome_index`, `won`, `pool_received`/`bettor_received`, `leverage` | | 96 | `pm_market_accepted_operation` | Эвалуатор — рынок запущен: оракул принял, self-oracle или авто-приём; замороженные условия + флаг `self_oracle` | | 97 | `pm_payout_operation` | Расчёт — на каждую активную ставку: `amount` (стейк), `side`/`outcome_index`, `payout` (**0 при проигрыше**) | +| 100 | `pm_ban_expired_operation` | Временный бан оракула/создателя истёк на `banned_until`: крон снял его (поля `account`, `oracle`, `creator`). Досрочное ручное снятие — через подписанный `pm_unban` | +| 101 | `pm_market_expired_operation` | Дедлайн `accept_deadline` ожидающего рынка прошёл: оракул не принял/отклонил в течение `pm_oracle_accept_window_sec` — рынок аннулирован, сид возвращён (комиссия за создание удержана). Поля `oracle`, `creator`, `market_id`, `refunded_liquidity` | + +> ID 91–93 — это *обычные* операции `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert` (см. +> спецификацию); ID 98–99 — *обычные* операции `pm_dispute_oracle_respond`/`pm_unban` (выше). Результаты +> по каждому беттору — `pm_payout`; помарочный `pm_auto_payout` остаётся маркером расчёта. --- diff --git a/@l10n/ru/docs/protocol/operations/validators.md b/@l10n/ru/docs/protocol/operations/validators.md index 845b5363c4..bdeca93009 100644 --- a/@l10n/ru/docs/protocol/operations/validators.md +++ b/@l10n/ru/docs/protocol/operations/validators.md @@ -163,4 +163,39 @@ --- +## `set_reward_sharing_operation` (ID 64) + +**Авторизация:** `active` `owner` + +**Разделение награды валидатора (HF13).** Валидатор выбирает пересылать часть своей блочной награды своим **стейкхолдерам** — аккаунтам, проголосовавшим за него, — пропорционально взвешенному по времени весу голоса. `sharing_rate` — эта доля в базисных пунктах; общий пул накапливается и распределяется в конце каждой эпохи через виртуальную операцию `stakeholder_reward`. + +| Поле | Тип | Описание | +|------|-----|---------| +| `owner` | `account_name_type` | Валидатор, устанавливающий свою долю разделения | +| `sharing_rate` | `uint16_t` | Доля блочной награды, пересылаемая стейкхолдерам, в базисных пунктах (0 = ничего, 10000 = 100%) | + +```json +[64, { + "owner": "alice", + "sharing_rate": 2500 +}] +``` + +- `sharing_rate` ограничен сверху значением 10000 (100%). +- Распределение идёт по **взвешенному по времени** весу голоса, поэтому недавно добавленные голоса получают меньшую долю, пока не «созреют». + +--- + +## `stakeholder_reward_operation` (ID 65) — виртуальная + +Эмитируется в каждую эпоху распределения, когда валидатор с ненулевым `sharing_rate` выплачивает стейкхолдеру его долю общей блочной награды. Виртуальная (никогда не подписывается); появляется в `account_history`. + +| Поле | Тип | Описание | +|------|-----|---------| +| `validator` | `account_name_type` | Валидатор, разделивший награду | +| `stakeholder` | `account_name_type` | Голосующий, получающий долю | +| `shares` | `asset` (SHARES) | Сумма, зачисленная стейкхолдеру | + +--- + См. также: [Типы данных](../data-types.md), [Обзор операций](./overview.md), [Свойства цепочки](../../governance/chain-properties.md). diff --git a/@l10n/ru/docs/protocol/virtual-operations.md b/@l10n/ru/docs/protocol/virtual-operations.md index a0d2b158da..909bbf4a06 100644 --- a/@l10n/ru/docs/protocol/virtual-operations.md +++ b/@l10n/ru/docs/protocol/virtual-operations.md @@ -351,4 +351,32 @@ --- +## Прогнозные рынки (HF14) + +Эмитируются логикой консенсуса PM — **не** кроном по часам. Два источника: +- **Эвалуатор подписанной операции**, в момент её применения — `pm_market_accepted` (при accept / self-oracle / авто-приёме) и `pm_leverage_liquidate` (при встречной или отменяющей ставке, толкающей плечевую позицию за порог). +- **Обработчик дедлайнов `process_pm_markets()`**, запускаемый каждый блок: рассчитывает рынки, достигшие **экспирации / дедлайна / окончания окна спора / границы эпохи** (кап `pm_processing_cap_per_block`, старейший дедлайн первым). + +См. [Операции прогнозных рынков](./operations/prediction-markets.md). (ID 91–93 — это *обычные* операции `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert`, а ID 98–99 — *обычные* операции `pm_dispute_oracle_respond`/`pm_unban` — см. ту страницу.) + +| ID | Операция | Триггер | +|----|----------|---------| +| 84 | `pm_batch_settle_operation` | Достигнута граница эпохи: очередные ставки по снимку начала эпохи | +| 85 | `pm_commit_forfeit_operation` | `reveal_deadline` без раскрытия: штраф → `forfeit_pool`, остаток возвращён | +| 86 | `pm_auto_payout_operation` | Истекло окно спора (по рынку): паримутюэль-расчёт + возврат принципала LP | +| 87 | `pm_dispute_finalize_operation` | Достигнут `voting_end_time`: подсчёт решает; штраф оракулу; пере-разрешение/поддержка | +| 88 | `pm_dispute_auto_close_operation` | Достигнут `auto_close_time`, оракул не ответил: анти-фриз возврат, слэш страховки → DAO | +| 89 | `pm_oracle_missed_penalty_operation` | Истёк `result_expiration` без разрешения: слэш → DAO, возврат всех ставок | +| 90 | `pm_lazy_recall_operation` | Простаивающая аллокация lazy-пула достигла шага отзыва: один поэтапный шаг возвращён в пул | +| 94 | `pm_leverage_liquidate_operation` | Эвалуатор — ликвидация плеча в ходе рынка (встречная `0` / отменяющая `1` ставка, каскад) | +| 95 | `pm_leverage_resolve_operation` | Расчёт — плечевая позиция принудительно закрыта по `cancel_value`: `outcome_index`, `won`, `pool_received`/`bettor_received`, `leverage` | +| 96 | `pm_market_accepted_operation` | Эвалуатор — рынок запущен: оракул принял, self-oracle или авто-приём; замороженные условия + флаг `self_oracle` | +| 97 | `pm_payout_operation` | Расчёт — на каждую активную ставку: `amount` (стейк), `side`/`outcome_index`, `payout` (**0 при проигрыше**); рядом с порыночной `pm_auto_payout` | +| 100 | `pm_ban_expired_operation` | Временный бан оракула/создателя истёк на `banned_until`: крон очистил его (`account`, `oracle`, `creator`). Досрочные ручные снятия используют подписанную `pm_unban` | +| 101 | `pm_market_expired_operation` | Дедлайн `accept_deadline` пендинг-рынка прошёл: оракул не принял/отклонил в течение `pm_oracle_accept_window_sec` — рынок аннулирован (`status -1`), сид-ликвидность возвращена (`refunded_liquidity`), комиссия за создание **не** возвращена (`oracle`, `creator`, `market_id`, `refunded_liquidity`) | + +Всё движение средств PM строго zero-sum (без эмиссии); расчёт сохраняет `Σ out == Σ ставок + принципал LP + forfeit_pool`. + +--- + См. также: [Обзор операций](./operations/overview.md), [Награды](./operations/awards.md), [Комитет](./operations/committee.md). diff --git a/@l10n/zh-CN/docs/advanced/hardfork-management.md b/@l10n/zh-CN/docs/advanced/hardfork-management.md index a2dbd920a3..47c5017731 100644 --- a/@l10n/zh-CN/docs/advanced/hardfork-management.md +++ b/@l10n/zh-CN/docs/advanced/hardfork-management.md @@ -48,6 +48,8 @@ VIZ Ledger 通过确定性硬分叉系统协调协议升级。硬分叉在编译 | 10 | 通胀模型 | | 11 | 发行模型变更 | | 12 | 紧急共识恢复(见下文) | +| 13 | 分配纪元长度(`chain_properties_hf13`) | +| 14 | 预测市场 (Onix):18 个操作 + 7 个虚拟操作,CPMM/LMSR、同注分彩结算、预言机、争议、承诺-揭示、懒惰池;链属性 v5 | --- diff --git a/@l10n/zh-CN/docs/consensus/emergency-consensus.md b/@l10n/zh-CN/docs/consensus/emergency-consensus.md index fbfc9a48e6..572c62204d 100644 --- a/@l10n/zh-CN/docs/consensus/emergency-consensus.md +++ b/@l10n/zh-CN/docs/consensus/emergency-consensus.md @@ -151,9 +151,9 @@ real_witness_slots >= CHAIN_MAX_VALIDATORS × 75% | 防护 | 紧急期间的行为 | |------|--------------| | `resync_from_lib()` | **完全跳过** — 紧急期间弹出 LIB 附近的区块会崩溃 | -| `stale_sync_check_task()` | 如果主节点头部推进 → 重置计时器,跳过恢复;如果从节点头部卡住 → 允许恢复 | +| `check_wedge_watchdog()` | **没有紧急模式的特殊分支。** 仅当头部冻结且远低于*经过佐证的*网络顶端(已建立连接对等节点中第二高的头部)时才会启动,任何头部推进都会重置计时器。除非设置 `auto-resync-on-wedge = true`,否则只记录日志;启用后确认的卡死会使节点退出 | | `handle_block()`(DLT,同步模式,间隔 0–2) | 视为正常(非同步)以防止生产循环中断 | -| 快照停滞同步检测 | 与停滞同步检查相同的逻辑 | +| 快照停滞同步检测(`enable-stalled-sync-detection`,默认关闭) | **主/从的特殊处理实际位于此处。** 超时时读取 `emergency_consensus_active`;若本节点是紧急主节点(持有紧急密钥且委员会在排程中),单独出块属于正常情况,因此跳过恢复。卡住的从节点则允许恢复 | `resync_from_lib()` 防护最为关键:紧急期间,LIB 接近 HEAD。将区块弹回 LIB 并重置 fork DB 会导致来自真实网络的节点区块链接到重新播种的 LIB,触发 fork 切换,弹出到已提交的 LIB 以下,要么崩溃要么损坏状态。 @@ -196,7 +196,7 @@ real_witness_slots >= CHAIN_MAX_VALIDATORS × 75% | 6 | `maybe_produce_block`(主节点) | 绕过同步、过时、参与;跳过少数派 fork | | 7 | `maybe_produce_block`(从节点) | 必须先同步;21 区块隔离检查 | | 8 | `resync_from_lib` | 紧急期间**完全跳过** | -| 9 | `stale_sync_check_task` | 主节点头部推进时跳过;从节点卡住时允许 | +| 9 | `check_wedge_watchdog` | 没有紧急模式分支;防止误触发的是顶端佐证和头部推进重置 | | 10 | `handle_block` | DLT 紧急中几乎追上的区块视为正常 | | 11 | `database::open` | 启动计划修复 | | 12 | `validator_guard` | 紧急期间不抑制密钥恢复 | diff --git a/@l10n/zh-CN/docs/governance/chain-properties.md b/@l10n/zh-CN/docs/governance/chain-properties.md index 4ca4e23efa..41d7487dfa 100644 --- a/@l10n/zh-CN/docs/governance/chain-properties.md +++ b/@l10n/zh-CN/docs/governance/chain-properties.md @@ -122,8 +122,28 @@ | `chain_properties_hf4` | 1 | HF4 | inflation_validator_percent、inflation_ratio_committee_vs_reward_fund、inflation_recalc_period | | `chain_properties_hf6` | 2 | HF6 | data_operations_cost_additional_bandwidth、validator_miss_penalty_percent、validator_miss_penalty_duration | | `chain_properties_hf9` | 3 | HF9 | create_invite_min_balance、committee_create_request_fee、create_paid_subscription_fee、account_on_sale_fee、subaccount_on_sale_fee、validator_declaration_fee、withdraw_intervals | +| `chain_properties_hf13` | 4 | HF13 | distribution_epoch_length | +| `chain_properties_pm` | 5 | HF14 | ~30 个预测市场参数 + 终止开关 `pm_commit_reveal_enabled`、`pm_lazy_pool_enabled` | -所有新的验证者属性提交请使用版本索引 3(`chain_properties_hf9`)。 +所有新的验证者属性提交请使用版本索引 **5**(`chain_properties_pm`)。索引 4 为 `chain_properties_hf13`(`distribution_epoch_length`)。 + +### 预测市场参数 (v5, HF14) {#pm-parameters} + +均为中位数投票;参见 [预测市场操作](../protocol/operations/prediction-markets.md)。 + +所有 PM 百分比均以 bp 计(10000 = 100.00%),与其他 `*_percent` 一致;不再使用千分比(‰)。 + +- **预言机:** `pm_min_oracle_insurance`、`pm_max_oracle_fee_percent`(**唯一**的费率治理上限——针对预言机 %)、`pm_oracle_registration_fee`、`pm_oracle_penalty_percent`、`pm_oracle_dispute_response_sec`、`pm_oracle_accept_window_sec`(默认 3600 = 1 小时——指定的预言机须在此窗口内接受或拒绝待定市场;超时后 cron 向创建者退还种子流动性,但**不**退还创建费,并作废市场 → `pm_market_expired`)。 +- **风险 / 覆盖率** *(市场下注量的百分比,100 = 1.0×):* `pm_listing_min_coverage_percent`(250 = 2.5×)——预言机保险覆盖低于其下注量此比例的市场,会从默认 `list_markets` 目录中隐藏(经 `show_risky` 显示);`pm_betting_min_coverage_percent`(150 = 1.5×)——建议性阈值,发布供客户端在下注前要求显式风险确认(不在链上强制;须 `≤ pm_listing_min_coverage_percent`)。 +- **市场:** `pm_min_liquidity`、`pm_market_creation_fee`、`pm_max_outcomes`、`pm_max_market_duration`。*(无聚合费率上限;creator/liquidity 费率无上限、自我约束;静态 `总和 ≤ 100%` 偿付不变式。)* +- **批次 / 承诺-揭示:** `pm_batch_epoch_blocks`、`pm_reveal_window_blocks`、`pm_min_batch_bet`、`pm_commit_no_reveal_penalty_percent`、`pm_commit_reveal_enabled`。 +- **争议:** `pm_dispute_fee`、`pm_dispute_grace_sec`、`pm_dispute_vote_period_sec`、`pm_dispute_auto_close_sec`、`pm_dispute_approve_min_percent`、`pm_no_contest_penalty_percent`、`pm_dispute_reward_multiplier`(bp 乘数,10000 = 1×)。 +- **时间惩罚:** `pm_default_time_penalty_percent`、`pm_max_time_penalty`。 +- **懒惰池:** `pm_lazy_pool_enabled`、`pm_lazy_alloc_percent`、`pm_lazy_max_total_alloc_percent`、`pm_lazy_recall_step_percent`、`pm_lazy_lock_sec`、`pm_lazy_emergency_penalty_percent`、`pm_lazy_min_liquidity_fee_percent`(默认 200 = 2%——池拒绝为 `liquidity_fee_percent` 低于此奖励下限的市场共同提供流动性)。 +- **杠杆(可选):** `pm_leverage_enabled`、`pm_leverage_fund_percent`、`pm_leverage_max_per_position_bp`、`pm_leverage_max_position_ratio_percent`、`pm_leverage_min_market_liquidity`、`pm_leverage_safety_margin_percent`、`pm_leverage_max_slippage_percent`、`pm_leverage_m_factor_percent`、`pm_leverage_pool_profit_percent`、`pm_leverage_expiration_buffer_sec`、`pm_conversion_profit_cost_percent`。 +- **公平性:** `pm_processing_cap_per_block`。 + +三个 `*_enabled` 标志(`pm_commit_reveal_enabled`、`pm_lazy_pool_enabled`、`pm_leverage_enabled`)为实时终止开关:验证者中位数可在无需新硬分叉的情况下停用承诺-揭示、懒惰池或杠杆。 --- diff --git a/@l10n/zh-CN/docs/node/configuration.md b/@l10n/zh-CN/docs/node/configuration.md index 5804cc6777..d7d8e1bc21 100644 --- a/@l10n/zh-CN/docs/node/configuration.md +++ b/@l10n/zh-CN/docs/node/configuration.md @@ -158,6 +158,11 @@ skip-virtual-ops = false # 允许在链过期时生产(仅用于开发/测试网) enable-stale-production = false +# 禁用少数派 fork 检测(仅用于单运营者测试网/分叉)。 +# 与 enable-stale-production 不同,它不会在健康参与度下被自动清除。 +# 切勿在真实的公共网络上启用。 +disable-minority-fork-detection = false + # 生产区块所需的最低参与度 % (0–99) required-participation = 33 @@ -203,4 +208,4 @@ logger.p2p.appenders = p2p | `plugins/chain/plugin.hpp` | `shared-file-size`, `min-free-shared-file-size`, `inc-shared-file-size`, `block-num-check-free-size`, `single-write-thread`, `enable-plugins-on-push-transaction`, `read-wait-micro`, `max-read-wait-retries`, `write-wait-micro`, `max-write-wait-retries`, `skip-virtual-ops`, `clear-votes-before-block`, `track-account-range`, `history-whitelist-ops`, `history-blacklist-ops`, `history-start-block` | | `plugins/p2p/p2p_plugin.hpp` | `p2p-endpoint`, `p2p-max-connections`, `p2p-seed-node`, `checkpoint` | | `plugins/webserver/webserver_plugin.hpp` | `webserver-http-endpoint`, `webserver-ws-endpoint`, `webserver-thread-pool-size` | -| `plugins/validator/validator.hpp` | `enable-stale-production`, `required-participation`, `validator`, `private-key`, `emergency-private-key`, `fork-collision-timeout-blocks`, `ntp-server`, `ntp-request-interval`, `debug-block-production` | +| `plugins/validator/validator.hpp` | `enable-stale-production`, `disable-minority-fork-detection`, `required-participation`, `validator`, `private-key`, `emergency-private-key`, `fork-collision-timeout-blocks`, `ntp-server`, `ntp-request-interval`, `debug-block-production` | diff --git a/@l10n/zh-CN/docs/node/validator-node.md b/@l10n/zh-CN/docs/node/validator-node.md index f59683fce1..a95ea3579b 100644 --- a/@l10n/zh-CN/docs/node/validator-node.md +++ b/@l10n/zh-CN/docs/node/validator-node.md @@ -164,6 +164,9 @@ docker run -d \ ### 少数派 Fork 检测 如果节点的 fork 数据库显示 21+ 个连续区块全部来自此节点自己的验证者,它会自动回滚到 LIB 并重新同步。这可以捕获网络隔离情况。 +> [!WARNING] 单运营者分叉 +> 在**一个运营者控制所有验证者**的测试网或主网分叉上,「连续 21 个区块都是我们的」是正常的健康状态,因此检测器会无限循环地回滚到 LIB。此时 `enable-stale-production = true` **无效**:在参与率 ≥33% 时,该覆盖会在每个区块被自动清除。请改用 `disable-minority-fork-detection = true`——它会完全绕过标准检测路径和 DLT 检测路径,并且永远不会被自动清除。**切勿在真实的公共网络上启用**——它会移除隔离保护。 + ### 生产 Watchdog 如果在 `should_be_producing` 为 true 的情况下 180 秒内(紧急主节点为 60 秒)没有生产区块,watchdog 会自动清除卡住的标志(`minority_fork_recovering`、P2P 追赶、链同步)并尝试恢复生产。 diff --git a/@l10n/zh-CN/docs/plugins/prediction-market-api.md b/@l10n/zh-CN/docs/plugins/prediction-market-api.md new file mode 100644 index 0000000000..6a350cfcd4 --- /dev/null +++ b/@l10n/zh-CN/docs/plugins/prediction-market-api.md @@ -0,0 +1,186 @@ +# `prediction_market_api` 插件 + +对 HF14 预测市场状态的只读 JSON-RPC 访问(市场、下注、预言机、流动性、争议、懒惰池、链属性 v5)。插件直接返回原始共识 `pm_*` 对象,外加少量计算型 DTO。 + +**启用:** 将 `prediction_market_api` 加入节点插件列表(`vizd` 默认注册)。依赖 `chain` + `json_rpc`。所有列表方法通过 `from`(跳过)与 `limit`(`≤ 1000`)分页。 + +## 方法 + +### 市场 + +| 方法 | 参数 | 返回 | +|------|------|------| +| `get_market` | `market_id` | `pm_market_object` | +| `list_markets` | `status, from, limit, [show_risky]` | `pm_market_object[]` | +| `list_markets_by_oracle` | `oracle, from, limit` | `pm_market_object[]` | +| `list_markets_by_creator` | `creator, from, limit` | `pm_market_object[]` | +| `get_market_outcomes` | `market_id` | `pm_outcome_object[]` | +| `get_market_weight_sums` | `market_id` | `pm_market_weight_sums`(计算型) | +| `get_market_bets` | `market_id, from, limit` | `pm_bet_object[]` | +| `get_market_liquidity` | `market_id, from, limit` | `pm_liquidity_object[]` | +| `get_market_full` | `market_id, [account]` | `pm_market_full`(计算型) | + +`list_markets` 的 `status`:`-1` 已删除、`0` 等待、`1` 活跃、`2` 关闭、`3` 已裁定。默认情况下 +`list_markets` 会隐藏保证金不足的市场(预言机保险 < 下注量的 **2.5×**);`show_risky = true` 可显示它们 +(仅隐藏,链上始终允许下注)。 + +`get_market_full` 是市场详情页的**单次调用富集视图**:返回市场 + 结果 + 权重合计 + 预言机(含可靠度)+ 已解析元数据,且——当提供可选 `account` 时——返回该账户**在本市场上**的下注、杠杆头寸与 LP。为瘦客户端省去多次往返。 + +### 市场元数据(链下解析) + +每个市场携带一个自由格式、共识不透明的 `metadata` JSON 字符串。插件将其索引的键(类别 / 子类别 / 标签 / +受禁司法辖区)解析为 `pm_market_meta_object`——**仅用于展示/索引,非共识**。 + +| 方法 | 参数 | 返回 | +|------|------|------| +| `get_market_meta` | `market_id` | `pm_market_meta_object`(无则报错) | +| `list_markets_by_category` | `category, from, limit, [jurisdiction], [subcategory], [tag], [sort]` | `pm_market_meta_object[]` | +| `get_market_categories` | — | `pm_market_categories`(计算型) | + +`list_markets_by_category` 会排除其 `banned_jurisdictions` 含可选 ISO 代码 `jurisdiction` 的市场(受监管 +客户端传入自身辖区即可只获取可列出的市场)。可选的 `subcategory`(精确)与 `tag`(CSV 成员)进一步收窄集合; +`sort` ∈ `newest`(市场 id 降序,默认)· `oldest` · `volume`(`bets_sum` 降序)· `expiration`(`betting_expiration` +升序)。`get_market_categories` 返回实时分类法——每类别 / 每子类别计数,外加前 20 个热门标签(排除 jurisdiction-*)—— +在当前已索引市场上聚合,使浏览 UI 无需硬编码分类法即可构建其筛选标签。对象:`market`、`category`、`subcategory`、`tags`(逗号分隔)、 +`banned_jurisdictions`(逗号分隔 ISO;为空 = 全球允许)、`expiry`(争议窗口关闭 + TTL 后清理)。 + +### 持仓与预言机 + +| 方法 | 参数 | 返回 | +|------|------|------| +| `get_account_positions` | `account, from, limit` | `pm_position[]`(下注 + `expected_payout`) | +| `get_account_leverage_positions` | `account, from, limit` | `pm_leverage_position_object[]` | +| `get_market_leverage_positions` | `market_id, from, limit` | `pm_leverage_position_object[]` | +| `get_creator_ban` | `account` | `pm_creator_ban_object`(无则报错) | +| `get_oracle` | `owner` | `pm_oracle`(对象 + `reliability_score`) | +| `list_oracles` | `from, limit` | `pm_oracle_object[]` | + +> 每位下注者的结算以 `pm_payout` 虚拟操作发出(本金、side/outcome、结果;输则为 `0`);杠杆头寸的结算为 +> `pm_leverage_resolve`(`outcome_index`、`won`、`leverage`)。两者均见于 `account_history`;头寸对象本身可经上述方法查询。 + +### 杠杆预览(Boost) + +只读报价,调用评估器所用的**同一套节点内保证金数学**,因此预览与对应 `pm_leverage_*` 操作在头区块的计算结果一致。它们是非共识估算(读取与广播之间储备会变动——务必发送链上滑点保护)。 + +| 方法 | 参数 | 返回 | +|------|------|------| +| `get_leverage_quote` | `market_id, outcome_index, collateral` | `pm_leverage_quote`(计算型) | +| `get_leverage_close_preview` | `position_id` | `pm_leverage_close_preview`(计算型) | +| `get_leverage_convert_preview` | `position_id` | `pm_leverage_convert_preview`(计算型) | + +`get_leverage_quote` 镜像 `pm_leverage_open`:返回最大偿付贷款与由此得到的最大杠杆、池/头寸上限、至多 12 个滑块档位(每档含代币、阈值、当前及最坏情形取消价值),且——当无法杠杆时——返回 `available = false` 并附 `failed_constraints[]` 列表。`get_leverage_close_preview` / `get_leverage_convert_preview` 在当前储备下镜像 `pm_leverage_close` / `pm_leverage_convert`(取消价值、池义务、下注者所得、是否可平仓/可转换,以及按当前中位数 `pm_conversion_profit_cost_percent` 的转换费)。 + +### 争议、懒惰池、治理 + +| 方法 | 参数 | 返回 | +|------|------|------| +| `get_dispute` | `market_id` | `pm_dispute_object` | +| `get_dispute_votes` | `market_id` | `pm_dispute_votes`(投票 + 实时计票) | +| `get_lazy_pool` | — | `pm_lazy_pool_object` | +| `get_lazy_deposit` | `account` | `pm_lazy_deposit_object` | +| `get_lazy_allocations` | `from, limit` | `pm_lazy_allocation_object[]` | +| `get_market_lazy_allocation` | `market_id` | `pm_lazy_allocation_object`(无则报错) | +| `get_pm_chain_properties` | — | `chain_properties_pm`(中位数, v5) | + +`get_lazy_allocations` 列出懒惰池的每市场分配记录(用于池仪表盘);`get_market_lazy_allocation` 获取给定市场的那一条。预言机罚分戳记无需单独方法——它们随 `pm_oracle_object`(`penalty_stamps`、`last_penalty_stamp_time`)经 `get_oracle` 一并返回。 + +### 图表 —— kline / 权重历史 + +用于绘制每个结果权重随时间变化的时间序列。**每当市场的各结果权重发生变化时**(下注、取消、清算、批次结算、杠杆开仓或杠杆结算),插件追加一个点 —— 即各结果同注分彩权重(下注额)的带时间戳快照。这是**非共识**插件状态(存于 chainbase,undo/redo 安全,不计入状态哈希);历史从插件在节点上首次启用时开始累积。 + +**保留:** kline 历史与市场元数据**一同清理**,遵循同一时间表 —— `result_expiration` + 争议宽限 + `pmm-ttl-days`(默认 7)。市场的完整图表在其存续期间及结算后的保留窗口内均可用,随后两个索引被清理(极长历史会分多个区块逐步清空),以使节点存储保持有界。 + +| 方法 | 参数 | 返回 | +|------|------|------| +| `get_market_kline` | `market_id, [from], [limit]` | `pm_kline[]`(按 `seq` 升序) | + +分页为**从最新偏移**(为瘦客户端刻意保持简单):`from` 为跳过的**最新**点数量,`limit ≤ 1000` 为页大小。 +- `(market_id, 0, 1000)` → 最新 ≤ 1000 个变化。 +- `(market_id, 1000, 1000)` → 再往前一页的 1000 个 —— 以 `from += 1000` 重复以惰性加载更早历史。 + +绘图:x = `timestamp`(unix 秒),每个结果 `i` 一条线,y = `weights[i]`(或归一化 `weights[i] / Σweights`,即隐含概率)。 + +## 计算型 DTO + +- **`pm_position`** —— 下注 + `expected_payout`(若该方获胜的赔付,或结算后已实现值;与 `settle_market` 逐字节一致)、`market_status`、`resolved_outcome`。 +- **`pm_oracle`** —— 预言机对象 + `reliability_score`(bp `[0..10000]`,非共识启发式:裁定成功率与争议胜率的混合,再减去封禁罚分)。 +- **`pm_market_weight_sums`** —— 各方/各结果的 `bets_sum`/`weight_sum`(权重通过扫描下注计算,因其不存储)。 +- **`pm_kline`** —— 一个图表点:`seq`(uint32,0 起、按市场单调递增的变化索引)、`timestamp`(unix 秒,x)、`reason`(uint8:0 下注、1 取消、2 清算、3 批次结算、4 杠杆开仓、5 杠杆结算)、`bets_sum`(总下注额)、`weights[]`(每个结果的权重,y;索引 = outcome_index)。 +- **`pm_dispute_votes`** —— 投票 + finalize 计票。旧字段(权重 = `|vote_percent|`,非质押):`uphold_weight`/`challenge_weight`/`total_weight`、`challenger_leads`(≥ `pm_dispute_approve_min_percent`)、`proposed_outcome`。**精确的按质押加权投影(镜像 `pm_dispute_finalize`;所有 `*_shares` 为 vesting-shares = `effective_vesting_shares` + 懒惰池质押→shares):** `participation_shares`(已投票者权重之和)、`electorate_shares`(`total_vesting_shares` + 池 NAV→shares)、`quorum_required_shares`、`quorum_percent_bp`(法定人数,bp,10000 = 100.00%)、`quorum_reached`(bool)、`oracle_defense_shares`/`change_shares`、`outcome_change_shares[]`(按结果)、`expected_uphold`(预言机裁决是否维持)、`expected_outcome`(当前若裁决将设定的结果)、`expected_consensus_strength_bp`。该投影与定时任务在 `voting_end_time` 按当前投票应用的结果一致(在此之前投票可更改)。 + +**`pm_market_full`** —— 单次调用的富集市场视图(`oracle`/`meta` 缺失时为 `null`;除非提供 `account` 参数,否则 `my_*` 数组为空): +``` +{ market: pm_market_object, + outcomes: pm_outcome_object[], // 二元市场为空 + weight_sums: pm_market_weight_sums, + oracle: pm_oracle | null, + meta: pm_market_meta_object | null, + my_positions: pm_position[], // 账户在本市场的下注 + my_leverage_positions: pm_leverage_position_object[], + my_liquidity: pm_liquidity_object[] } +``` + +**`pm_leverage_quote`** —— 杠杆开仓预览(来自 `pm::leverage::*`,即评估器运行的同一套数学): +``` +{ available: bool, outcome_index, collateral, + max_loan, max_leverage_x100, // 100 = 1.00× + pool_free_amount, fund_available, per_position_cap, market_position_cap, + pool_profit_percent, safety_margin_percent, max_slippage_percent, m_factor_percent, + expiration_buffer_sec, auto_close_time, // betting_expiration − buffer + stops: [ { leverage_x100, loan, total_bet, expected_tokens, pool_profit, + liquidation_threshold, current_cancel_value, worst_case_cancel_value } ], + failed_constraints: [ { constraint, reason } ] } // !available 时填充 +``` +**`pm_leverage_close_preview`** —— `{ position_id, outcome_index, cancel_value, pool_obligation, bettor_receives, collateral, loan, pool_profit_charge, closeable: bool, loss_vs_collateral, loss_percent_bp }`。 +**`pm_leverage_convert_preview`** —— `{ position_id, outcome_index, cancel_value, pool_obligation, current_profit, conversion_profit_cost_percent, conversion_fee, total_user_payment, convertible: bool }`。 + +**`pm_market_categories`** —— 带实时计数的浏览分类法: +``` +{ categories: [ { category, count, subcategories: [ { subcategory, count } ] } ], // 按 count 降序 + hot_tags: [ { tag, count } ] } // 前 20(排除 jurisdiction-*) +``` + +## 示例 + +市场 `42` 最新 1000 个图表点,再取前 1000 个: +```bash +# 最新一页 +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_market_kline",[42,0,1000]]}' http://127.0.0.1:8090 +# 往前一页 +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_market_kline",[42,1000,1000]]}' http://127.0.0.1:8090 +``` + +瘦客户端(向后滚动时惰性加载更早历史)—— 将每个点按结果转成 `{ x: unixtime, y: weight }` 序列: +```js +async function call(method, params) { + const r = await fetch('http://127.0.0.1:8090', { method: 'POST', + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'call', + params: ['prediction_market_api', method, params] }) }); + return (await r.json()).result; +} + +// 从最新向后按每页 1000 拉取,直到凑够 `want` 个点(或历史耗尽)。 +async function loadKline(marketId, want = 3000) { + const points = []; + for (let from = 0; points.length < want; from += 1000) { + const page = await call('get_market_kline', [marketId, from, 1000]); + if (!page.length) break; // 到达历史起点 + points.unshift(...page); // 页内升序;更早的页前置 + if (page.length < 1000) break; + } + return points; +} + +// 每个结果一条 {x,y} 序列 —— 直接喂给任意图表库。 +function toSeries(points, outcomeCount) { + const series = Array.from({ length: outcomeCount }, () => []); + for (const p of points) + for (let i = 0; i < outcomeCount; i++) + series[i].push({ x: p.timestamp, y: Number(p.weights[i]) }); + return series; +} +``` + +参见 [预测市场操作](../protocol/operations/prediction-markets.md) 与 [链属性](../governance/chain-properties.md#pm-parameters)。 diff --git a/@l10n/zh-CN/docs/plugins/validator.md b/@l10n/zh-CN/docs/plugins/validator.md index 1b8ee701de..d6215fe8a2 100644 --- a/@l10n/zh-CN/docs/plugins/validator.md +++ b/@l10n/zh-CN/docs/plugins/validator.md @@ -24,6 +24,7 @@ chain::plugin, p2p::p2p_plugin, snapshot::snapshot_plugin | `private-key` | — | 用于签名的 WIF 私钥;可重复 | | `emergency-private-key` | — | 紧急共识的 WIF 密钥;自动将 `CHAIN_EMERGENCY_VALIDATOR_ACCOUNT` 添加到验证者集合 | | `enable-stale-production` | `false` | 绕过参与度和同步检查(仅用于测试网/网络恢复) | +| `disable-minority-fork-detection` | `false` | 完全跳过少数派 fork 检测(仅用于单运营者测试网/分叉)。在健康参与度下永远不会被自动清除——参见[少数派 Fork 检测](#少数派-fork-检测) | | `required-participation` | `3300` | 最低验证者参与度(**基点**,3300 = 33%) | | `fork-collision-timeout-blocks` | `21` | 强制生产前的连续 fork 冲突延迟次数(一个完整的验证者轮次) | @@ -116,7 +117,8 @@ T=6.000s 的槽位: 在每次生产尝试之前(在 HF12 安全检查之后),插件遍历 `fork_db` 中最后 21 个区块。如果所有 21 个都由节点自己配置的验证者生产,则节点被隔离在少数派 fork 上。 - **默认操作:** 调用 `p2p().resync_from_lib()` — 回滚到 LIB,重置 fork DB,重新启动 P2P 同步,重新连接种子节点。返回 `minority_fork`。 -- **使用 `enable-stale-production=true`:** 记录警告,继续生产。 +- **使用 `enable-stale-production=true`:** 记录警告,继续生产。**注意:** 在参与度 ≥33% 时,该覆盖会在每个区块被自动清除,因此在单运营者分叉上它*不会*停止检测器——请改用 `disable-minority-fork-detection`。 +- **使用 `disable-minority-fork-detection=true`:** 完全跳过标准检测路径和 DLT 检测路径,且该标志永远不会被自动清除。适用于「连续 21 个区块都是我们的」为健康稳态的单运营者测试网/分叉。**切勿在真实的公共网络上启用**——它会移除隔离保护。 - **跳过时机:** 紧急共识激活时(committee 区块总会匹配我们配置的集合)。在紧急模式下,DLT 特定的从节点隔离检查取代它。 --- @@ -217,7 +219,7 @@ validator[skip_flags=0x0 catching_up=0 head=#79881136 last_prod=45s_ago minority | `no_private_key` | 配置中缺少链上注册的签名密钥对应的 `private-key` | | `low_participation` | 网络参与度 < 33%;检查节点连接或设置 `enable-stale-production=true` | | `fork_collision` | 下一高度有竞争区块;等待投票权重解决或 21 次延迟超时 | -| `minority_fork` | 已隔离;插件自动重新同步到 LIB | +| `minority_fork` | 已隔离;插件自动重新同步到 LIB。在单运营者分叉上会无限循环——请设置 `disable-minority-fork-detection=true` | | Watchdog 重复触发 | 同步或追赶标志卡住;头部推进时 watchdog 会自动清除 | | `SLOT-HIJACK` 日志 | 紧急主节点清空了我们的密钥;通过 `validator_update_operation` 恢复 | diff --git a/@l10n/zh-CN/docs/prediction-markets/concepts-analysis.md b/@l10n/zh-CN/docs/prediction-markets/concepts-analysis.md new file mode 100644 index 0000000000..c2b271258d --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/concepts-analysis.md @@ -0,0 +1,204 @@ +--- +title: 概念分析 —— Onix 对照 90 个预测市场概念 +description: VIZ 链上实现(Onix 协议、Forecaster 客户端)如何映射到 90 个预测市场理论概念——哪些已解决、固有、不需要或在路线图中。 +--- + +# 概念分析 —— Onix 对照 90 个理论概念 + +> **Forecaster** 是 VIZ 链上预测市场的瘦客户端——让全球用户通过签署 `pm_*` 操作参与的接入层 +> (见[板块总览](./))。**Onix** 是它所对接的协议。本页将 **90 个 PM-Atlas 预测市场概念**逐一映射到 +> **VIZ 链上实现如何处理它**,以及该概念对本架构**是否必要**。 +> +> 依据文档:[白皮书](./whitepaper)、[规范](./specification)、[工作流与争议](./workflows)。 + +## 图例 + +| 标记 | 含义 | +|------|---------| +| ✅ **已解决** | Onix 设计直接解决/处理 | +| ⚪ **固有** | 由构造自然具备(无需额外工作) | +| ➖ **不需要** | 在 Onix 下架构上不必要 | +| 🟡 **部分 / 路线图** | 今日部分解决;其余在 VIZ 路线图 | +| 🏛 **客户端层** | 由司法辖区客户端处理,而非协议 | +| 🔴 **开放 / 风险** | 仍是现实关切;未完全解决 | + +与其他平台最大的结构性差异:**LP 本金结构性保障(赢家只从输家被没收的本金中获得赔付),二元用 CPMM、多元用 +LMSR-softmax + 同注分彩结算,且无订单簿。** 大量「流动性与交易」概念存在的目的就是管理做市商库存风险,因此对 +Onix **不适用**。 + +> **链上实化(HF14 / 上线)。** 本映射最初针对白皮书/规范撰写。若干当时标为*路线图*的项现已**实现为共识操作** +> 并在 `consensus_sim` 验证:批量拍卖 + 提交-揭示下注(`pm_commit_bet`/`pm_reveal_bet`/`pm_batch_settle`, +> 二元,中位数 kill-switch `pm_commit_reveal_enabled`);可选杠杆子系统(`pm_leverage_open/close/convert`, +> 懒惰池提供资金,kill-switch `pm_leverage_enabled`);懒惰池本身;`endogeneity_tier` 字段;链上创建者封禁; +> 更丰富的结算虚拟操作(`pm_payout`、`pm_leverage_resolve`、`pm_market_accepted`)+ 配套插件 API;以及懒惰 +> 池质押在 PM 争议与 DAO 提案投票中计为治理权重。**刻意未做:** 争议的提交-揭示投票——委员会争议按设计为公开 +> 听证。**仍是路线图:** 自动外生数据预言机。 + +## 1. 信息论 + +| # | 概念 | 裁定 | VIZ 上如何处理 / 是否需要 | +|---|---------|---------|--------------------------------------------------| +| 1 | **Brier Score** | ⚪ 固有 | 非协议机制,而是评判 Onix 市场的*指标*。每笔下注/裁定都是共识事件,价格史与结果全在链上 → 任何人可对平台(及预言机)做 Brier 评分。仅作分析/声誉输入,非核心逻辑。 | +| 2 | **Calibration** | ⚪ 固有 | Onix 价格是真实概率(CPMM、LMSR softmax 之和为 1)。校准是可*测量*的涌现属性,由时间惩罚与深度 LP 间接改善;协议不强制。 | +| 3 | **Credibility Markets** | 🟡 部分 | 保证金预言机 + 14 项声誉 + 综合信任分本质上是*裁决者*的信誉市场。「以信誉质押于结果」是原生的。通用「为主张质押信誉」产品可在客户端层构建,非核心。 | +| 4 | **Distribution Markets** | 🟡 路线图 | Onix Multi(3–10 离散结果)以桶近似分布。真正的连续分布市场(CDF/标量)当前**不**在范围;需标量结果操作。 | +| 5 | **Endogeneity** | 🟡 部分(已缓解) | 市场改变其所预测之物的风险。由**已上线的 `endogeneity_tier` 字段**与**可选提交-揭示/批量下注(已上线)**缓解,使公开价格在窗口内不「泄露」;外生裁定仍是路线图。见[缓解](#fanshenxing-jiazu-de-huanjie)。 | +| 6 | **Forecasting Accuracy** | ⚪ 固有 | 整体价值主张。Onix 间接改善:无风险 LP → 更深订单 → 更少滑点 → 更多知情参与 → 更好价格。 | +| 7 | **Info Finance** | ⚪ 固有 | Onix *就是*信息金融工具:共识操作把信息变为可定价、可结算的头寸。迁移到 VIZ 使信息层抗审查、可组合。 | +| 8 | **Information Aggregation** | ✅ 已解决 | 核心功能。CPMM/LMSR 把分散下注聚合为单一概率;无风险 LP 深度正是使聚合奏效的杠杆。 | +| 9 | **Information Asymmetry** | 🟡 部分 | 同注分彩/CPMM 意味着知情者从*其他输家*而非 LP 获利——故不会拖垮流动性。**可选提交-揭示 + 批量下注现已上线(二元):** 承诺下注按统一批价结算,消除 mempool 方向泄露;per-market `allow_batch` + 中位数 kill-switch 保持可选。 | +| 10 | **Legibility** | ✅ 已解决 | 每个金融动作都是共识 VIZ 操作(前/后储备审计)。任何节点可读可审——远比中心化后端或不透明 CLOB 更清晰。新结算虚拟操作(`pm_payout`、`pm_leverage_resolve`、`pm_market_accepted`)+ 插件方法使逐下注者结果与杠杆结算可直接查询。 | +| 11 | **Longshot Bias** | 🟡 部分 | CPMM/LMSR 仍可能因下注行为出现冷门偏差;Onix 不直接纠正。时间惩罚与深度抑制扭曲,但偏差是行为输出,未消除。 | +| 12 | **Noise Decomposition** | ⚪ 固有 | 分析视角,非协议特性。链上价格/成交量序列使「信号 vs 噪声」分解对分析者可行。 | +| 13 | **Nowcasting** | ⚪ 固有 | Onix 价格随每笔下注更新(~3 秒块),给出实时即时预测。对任何活跃 AMM 市场固有。 | +| 14 | **Price Discovery** | ✅ 已解决 | CPMM 与 LMSR-softmax 是连续价格发现引擎;价格一致性(`Σ price = 1`)按构造成立,无需套利/拆合层。 | +| 15 | **Probability Infrastructure** | ✅ 已解决 | 本质上即 Onix 在 VIZ 的论点:预测市场作为**一等公民共识操作**(`pm_*`),而非智能合约——一种底层概率原语。 | +| 16 | **Superforecasting** | ⚪ 固有 | 个体技能概念;Onix 通过输家→赢家赔付奖励准确下注者。是参与者特征,非协议逻辑。 | +| 17 | **Wisdom of Crowds** | ✅ 已解决 | Onix 货币化的机制。无风险 LP 降低门槛,让更多群体参与,锐化聚合。 | +| 18 | **Yes Bias** | 🟡 部分 | 行为上偏向「Yes」。对称 CPMM 与仅作用于利润的时间惩罚结构上不偏 Yes,但也不纠正人类偏差。 | + +## 2. 机制设计 + +| # | 概念 | 裁定 | VIZ 上如何处理 / 是否需要 | +|---|---------|---------|--------------------------------------------------| +| 19 | **Binary Contracts** | ✅ 已解决 | Onix Binary = 两结果上的 CPMM(`x·y=k`)。AM-GM 证明保证 `reserve_a+reserve_b ≥ L`,覆盖 LP 本金。主市场类型。 | +| 20 | **Combinatorial Prediction Markets** | ➖ 不需要(今日) | LMSR 可在组合空间自然分解,但 Onix Multi 限于 3–10 个*独立*结果并刻意省略 CTF 拆/合。组合/条件捆绑明确不在范围。 | +| 21 | **Incentive Compatibility** | ✅ 已解决 | LMSR 从对数评分规则继承 IC(说真话占优)。Onix 再加保证金预言机(保险 > 操纵利润)、输家资助赢家、时间加权 LP 奖励对齐激励。 | +| 22 | **Keynesian Beauty Contest** | ✅ 已解决(市场)/ ⚪(争议层——按设计接受) | KBC 是*相对/对等评分*的病态。Onix **市场**层让下注者对外部真相(同注分彩)获酬,故结构上反 KBC——价格错时你因*偏离*而获利。唯一暴露是**按质押加权的委员会争议投票**。**决定:争议提交-揭示投票不会实现**——委员会争议是**公开听证**,DAO 信誉依赖透明裁决。残余 KBC 风险被接受且很小:投票者不因与多数一致而获酬,投票可改至关闭。池内 DAO 成员亦有投票权(懒惰池质押 → vesting-shares,HF14)。见[缓解](#fanshenxing-jiazu-de-huanjie)。 | +| 23 | **LMSR** | ✅ 已解决(关键创新) | LMSR 在二元上*失败*(0/1 边界永久损失)。Onix 之解:**二元用 CPMM,LMSR 仅用于多元且做市商非对手方**——同注分彩由输家付赢家,故 LMSR 补贴从不受险(`LP 最大损失 = 0` vs `b·ln(N)`)。核心设计动作。 | +| 24 | **LOX(Log-Odds Excess Lateness)** | ➖ 不需要 | 一种专门的评分/迟到指标。Onix 改用**对利润的二次时间惩罚**处理迟下注激励——更简单的结算期机制。 | +| 25 | **Market Manipulation** | 🟡 部分 | 保证金预言机(保证金须超操纵利润)、DPoS 校验操作、委员会争议提高操纵成本。靠深度限制大额下注操纵;在**批量/提交-揭示市场(已上线)**上由统一价中和速度狙击(「狙击税」);主动监控仍是路线图。 | +| 26 | **Market Scoring Rules** | ✅ 已解决 | Onix Multi 是市场评分规则(LMSR)实现,配以同注分彩结算。直接采用。 | +| 27 | **Multi-Outcome Markets** | ✅ 已解决 | Onix Multi 以 LMSR softmax + 同注分彩处理 N=3–10,`b = S/ln(N)`。一等公民市场类型。 | +| 28 | **Parimutuel Markets** | ✅ 已解决(基础) | *两种*市场类型的结算均为同注分彩:输家被没收的本金形成 `winners_pool`,按代币份额分配。这使 LP 保障是结构性而非保险性的。 | +| 29 | **Peer Prediction** | ➖ 不需要 | 无真相的说真话方案。Onix 依赖保证金预言机 + 委员会争议,不采用对等预测评分。 | +| 30 | **Position Collateralization** | ✅ 已解决(+ 可选杠杆,上线) | 默认每笔下注全额预付(无下注时扣费)——构造上完全抵押。Onix 现**亦**提供该概念的「下一步」:**可选杠杆子系统**(`pm_leverage_open/close/convert`,kill-switch 默认关)。保证金为**来自懒惰池的贷款**(不增发——保持零和),故系统视角持仓足额抵押。会击垮 CLOB 清算引擎的二元「跳空风险」由**按下注前储备清算**处理:对向下注/结算强平回收 `min(cancel_value, obligation) ≥ loan`;唯一有界坏账路径是同侧 `pm_cancel_bet`。清算级联刻意**不**受 kill-switch 门控。 | +| 31 | **Proper Scoring Rules** | ✅ 已解决 | LMSR 是对数恰当评分规则的成本函数对偶;Onix Multi 继承其真实诱导属性。 | +| 32 | **Reflexivity** | 🟡 部分(已缓解) | endogeneity 的母概念。由同一套件缓解——**提交-揭示/批量 + `endogeneity_tier` 已上线**,外生裁定仍路线图——**外加链上创建者封禁**(`pm_creator_ban_object`)应对有害反身性(暗杀/「悬赏」、为结果制造「拥趸」的宣传市场);类别*清单*本身在客户端层。深度无风险 LP 亦抬高制造头条操纵的成本。见[缓解](#fanshenxing-jiazu-de-huanjie)。 | + +## 3. 流动性与交易 + +> **要点:** Onix **无订单簿、无承担库存的做市商**。这一大类概念专为管理 CLOB/做市商库存风险而存在,因此对 Onix **不适用**。 + +| # | 概念 | 裁定 | VIZ 上如何处理 / 是否需要 | +|---|---------|---------|--------------------------------------------------| +| 33 | **Adverse Selection** | ✅ 已解决(重构) | 经典问题(知情流拖垮做市商)**无法拖垮 Onix LP**:赢家由输家付,从不动 LP 本金。知情者从其他*下注者*获利,非 LP。消除核心 LP 失效模式。 | +| 34 | **Arbitrage** | ⚪ 固有 | 市场内套利不必要:CPMM 与 LMSR-softmax 中价格一致性按构造成立。无需拆/合套利层。 | +| 35 | **Batched Auctions** | ✅ 已解决(可选,上线) | 实现为 **per-market 统一价批量**(`mode=1` + 提交-揭示 → 每 `pm_batch_epoch_blocks` 纪元 `pm_batch_settle`);仅净残量推动 AMM,同侧成交统一定价,中和速度狙击。per-market `allow_batch` + 中位数 kill-switch;今日**仅二元**。LP `Σreserve ≥ L` 不变量不变。 | +| 36 | **Bid-Ask Spread** | ➖ 不需要 | 无订单簿 → 无报价价差。「交易成本」表现为由深度决定的 CPMM/LMSR 滑点。概念不映射。 | +| 37 | **Bonding Trades** | ⚪ 固有 | 下注*就是*绑定交易:资金投入储备,仅在裁定(或撤注/转让)时释放。原生行为。 | +| 38 | **Continuous Double Auction** | ➖ 不需要 | CDA 是 Onix 明确放弃、改用 AMM 定价的 CLOB 模型。 | +| 39 | **Covariance Markets** | ➖ 不需要 | 交易事件间相关性需 Onix 刻意省略的组合/条件结构。范围外。 | +| 40 | **Cross-Platform Arbitrage** | 🟡 部分 | Onix 价格可与 Polymarket/Kalshi 背离;跨平台套利可行但在协议外。VIZ 开放 API + 无头客户端使数据可得;无原生跨平台桥。 | +| 41 | **Execution Quality** | ✅ 已解决(重构) | 无部分成交/排队。执行质量 = 确定性滑点 + 可选 `min_tokens`/`min_return` 保护,共识校验。构造上可预测。 | +| 42 | **Gap Risk** | ✅ 已解决(对 LP) | 跳空风险(突跳 0/1 抹掉做市商)正是 Onix 结构性 LP 保障消除的失效模式——LP 从不持有输家侧的终端风险。下注者仍承担自身结果风险(如设计)。 | +| 43 | **Hedging** | 🟡 部分 | 下注者可通过对冲头寸、转让(`pm_transfer_position`)、撤注(若允许)经反向 CPMM,及现在的**可选杠杆**(`pm_leverage_open/convert`)做资本高效对冲。无原生多腿衍生品;基础 + 杠杆对冲可行。 | +| 44 | **Implied Correlation** | ➖ 不需要 | 需 Onix 省略的多事件/组合市场。范围外。 | +| 45 | **Insider Trading** | 🟡 部分 / 🏛 客户端 | 协议无法侦测内幕;由时间惩罚(迟到的知情下注利润更少)与保证金裁定缓解。KYC/监控由**客户端层**负责(受监管客户端)。 | +| 46 | **Kelly Criterion** | ⚪ 固有 | 下注者下注规模策略,非协议特性。Onix 暴露干净概率与完全抵押,便于参与者计算 Kelly;新的**可选杠杆**让下注者以保证金践行分数 Kelly 优势(池资助、受清算约束)。 | +| 47 | **Liquidity Fragmentation** | 🟡 路线图 | 今日 per-market 池碎片化流动性。白皮书首要路线图项——**共享/类别级 AMM 池**——是架构性修复。懒惰池已跨市场共担*存款*。 | +| 48 | **Liquidity Provision** | ✅ 已解决(核心差异化) | 无风险 LP 是头条:本金结构性保障、时间加权费用奖励、懒惰池自动分配 + MasterChef 记账。解决了「LP 亏钱」这一驱动整个协议的问题。 | +| 49 | **Market Making** | ✅ 已解决(重构) | 无需主动做市商——AMM + LP 池*即*做市商,且不承担库存风险。「做市」坍缩为被动、无风险的流动性提供。 | +| 50 | **Minimum Viable Liquidity** | ✅ 已解决 | 强制下限:最低初始流动性 100 VIZ;懒惰池以 `free_balance × 分配%` 自动为每个新市场播种。MVL 结构性引导而非听天由命。 | +| 51 | **Order Book** | ➖ 不需要 | Onix 基于 AMM;按设计无订单簿。 | +| 52 | **Orderflow Arbitrage** | ➖ 不需要 | 无订单簿 / 无 PFOF 式订单流路由 → 不适用。 | +| 53 | **Relative Value Trading** | ➖ 不需要 | 跨工具相对价值需 Onix 省略的相关/组合市场。范围外。 | +| 54 | **Retail Flow** | ✅ 已解决(重构) | CLOB 模型中零售流补贴做市商对毒性流的亏损。Onix 无做市商需保护——零售与知情下注者同入一个同注分彩池;LP 无所谓。「零售 vs 毒性」张力在 LP 层消解。 | +| 55 | **Semantic Tick Size** | ➖ 不需要 | 最小变动价位是 CLOB 概念。Onix 价格是连续 AMM 函数;精度为固定 mVIZ 单位(1/1000)。无需 tick 设计。 | +| 56 | **Temporal Arbitrage** | 🟡 部分 | 早/晚下注风险不同;Onix 对利润的**时间惩罚**正是把迟到计入价格、抑制「等确定」套利的机制。未消除,但明确不鼓励。 | +| 57 | **Time Arbitrage** | 🟡 部分 | 与 #56 同族——利用信息时机。二次时间惩罚 + ~3 秒块降低但不消除优势。 | +| 58 | **Toxic Flow** | ✅ 已解决(对 LP) | 定义性 CLOB/LMSR 问题(狙击手在 99¢ 结果上以 10¢ 扫单,做市商吃 80¢)**不打到 Onix LP**——赔付来自输家本金,LP 补贴无条件返还。毒性流仅意味着知情下注者赢得同注分彩池,如设计。重大结构性胜利。 | +| 59 | **Wash Trading** | 🟡 部分 / 🏛 客户端 | 下注无即时费用消除了一个 wash 激励,但刷量仍可能;转让纯属重新分配(无费用农场)。侦测/监控是客户端 + 路线图关切。 | + +## 4. 预言机与裁定 + +| # | 概念 | 裁定 | VIZ 上如何处理 / 是否需要 | +|---|---------|---------|--------------------------------------------------| +| 60 | **Corruption Value Multiple(CVM)** | ✅ 已解决(设计原则) | 协议显式安全不变式:预言机**保险保证金须超潜在操纵利润**。风险因子(保险/下注比)馈入综合信任分。CVM 直接是保证金理据。 | +| 61 | **Dispute Resolution** | ✅ 已解决 | 完整系统:12h 宽限、`dispute_fee`、强制预言机响应、per-market 裁决(`dispute_mode==0` 委员会加权投票 / `==1` 指定裁决者)、保险罚没、3 结果 no-contest、14 天自动关闭防冻结、链上创建者/预言机封禁。**HF14 新增:** 懒惰池存款人保留争议投票权重(池 NAV → vesting-shares)。 | +| 62 | **Oracle Design** | ✅ 已解决 | 保证金预言机模型:注册费、≥5000 VIZ 保险、显式接受、带证据裁定、14 项声誉、新鲜度衰减、封禁机制。核心子系统。 | +| 63 | **Resolution Criteria** | 🟡 部分 / 🏛 客户端 | 市场问题/标准存于 `url`/描述(仅展示)。协议强制*流程*(谁裁、争议)而非标准*质量*——含糊标准是创建者/客户端责任,事后经争议 + **链上创建者封禁**(`pm_creator_ban_object`,已上线)治理。 | +| 64 | **Self-Resolving Markets** | ➖ 不需要(今日) | Onix 裁定由预言机驱动,非算法自裁。自动数据预言机是客观市场的高优路线图,可近似自裁。 | +| 65 | **UMA Protocol** | ➖ 不需要(被替代) | UMA 乐观预言机(Polymarket 使用)由 Onix 保证金预言机 + VIZ 委员会争议模型功能性替代。同一问题,原生 VIZ 之解——无外部预言机依赖。 | + +## 5. 治理与决策 + +| # | 概念 | 裁定 | VIZ 上如何处理 / 是否需要 | +|---|---------|---------|--------------------------------------------------| +| 66 | **Attention Markets** | ➖ 不需要 | 交易注意力/病毒性是独立产品;Onix 聚焦事件裁定。可为客户端层市场类别,非核心。 | +| 67 | **Conditional Tokens** | ➖ 不需要(明确) | 白皮书 §7.2 论证 CTF 拆/合**架构上不必要**——价格一致性是数学性的,非由代币强制。CTF *唯一*有用特性(可转让头寸)原生重实现为带加密 memo 的 `pm_transfer_position`。刻意省略。 | +| 68 | **Decision Markets** | 🟡 可能 | Onix Multi 可表达决策市场,但条件「若政策则指标」结构非原生(无条件代币)。可在客户端层构建。 | +| 69 | **Futarchy** | 🟡 可能(客户端) | futarchy = 条件期货上的决策市场。Onix 无原生条件市场,故完整 futarchy 核心不支持。VIZ 加权委员会已治理*参数*;按市场治理为客户端/路线图构建。 | +| 70 | **Hyperstition Markets** | ➖ 不需要(刻意) | 反身性即特性(协调而非预测)。这是*设计选择,非待修缺陷*:Onix 要求结果**可由保证金预言机外部核实**,按默认结构性排除 hyperstition 市场。可作客户端层「协调市场」产品存在,但非核心目标。见[缓解](#fanshenxing-jiazu-de-huanjie)。 | +| 71 | **Impact Markets** | ➖ 不需要 | 追溯资助/影响力证书市场是独立领域。可为客户端层类别;非核心。 | +| 72 | **No-Loss Prediction Markets** | 🟡 相邻 | Onix 对*下注者*非无损(输家没收本金以资助赢家)。但对 *LP* **是**「无损」(本金保障)。收益资助的无损变体是另一模型;LP 侧无损已交付。 | +| 73 | **Opportunity Markets** | ⚪ 固有(相邻) | 懒惰池的**机会成本保护**(渐进召回、活跃市场惩罚、故障印记)直接应对资本机会成本——尽管「机会市场」作为产品类别在范围外。 | + +## 6. 商业与平台 + +| # | 概念 | 裁定 | VIZ 上如何处理 / 是否需要 | +|---|---------|---------|--------------------------------------------------| +| 74 | **AI agents** | 🟡 路线图 | 无头客户端 + 开放 VIZ 操作使程序化代理(下注者、LP、自动预言机)易于实现。AI 驱动的流动性/裁定是自然延伸,尚未规范。 | +| 75 | **Cross-subsidization** | ⚪ 固有 | 懒惰池以一次存款跨多市场交叉补贴流动性;MasterChef `reward_per_share` 共享费用收益。交叉补贴内建于池经济。 | +| 76 | **Demand markets** | ➖ 不需要 | 衡量/聚合需求的市场是产品类别;非核心 Onix 原语。客户端层。 | +| 77 | **Distribution moat** | 🟡 战略 | Onix 护城河是无风险 LP 收益 + VIZ 原生基础设施(飞轮)。分发是 go-to-market 关切,迁移后由平台无关性部分解决。 | +| 78 | **Election markets** | 🏛 客户端 | 作为普通二元/多元市场支持;其*合法性*是司法辖区客户端事务(白名单预言机、类别过滤)。协议中立。 | +| 79 | **Event contracts** | ⚪ 固有 | 每个 Onix 市场*就是*事件合约。其监管分类是客户端/法律问题,非协议逻辑。 | +| 80 | **Federal preemption** | 🏛 客户端(对协议不适用) | 取决于美国事件合约是否「swaps」。VIZ DLT 是**基础设施而非运营方**——如比特币是账本。法律义务附着于客户端,非共识。 | +| 81 | **Long-tail markets** | ✅ 已解决 | 正是 LMSR 有界损失所启用的利基——Onix 经懒惰池自动分配 + 最低流动性下限使其播种*无风险*。长尾可行性是核心卖点。 | +| 82 | **Market structure** | ✅ 已解决(已定义) | Onix 定义清晰结构:AMM 定价、同注分彩结算、保证金预言机、DPoS 治理参数、共识级操作。相对 CLOB 平台的连贯、新颖市场结构。 | +| 83 | **Market surveillance** | 🟡 路线图 / 🏛 客户端 | 完整链上审计轨迹(每操作共识校验)使任何人*可*监控。主动监控/执法是客户端 + 路线图关切。 | +| 84 | **Network effects** | 🟡 战略 | 飞轮(无风险 LP → 深度 → 下注者 → 费用 → 更多 LP)是预期网络效应。共享流动性池(路线图)加强它。 | +| 85 | **Parlays** | ➖ 不需要 | 多腿组合下注需 Onix 省略的条件/组合结构。范围外(可在独立市场上由客户端构建)。 | +| 86 | **Platform competition** | 🟡 战略 | 以「无无常损失的被动收益」独特角度对抗 Polymarket/Kalshi。战略定位,非协议逻辑。 | +| 87 | **Polymarket** | ⚪ 参照 | 主要基准。Onix 在每个维度都不同:CPMM/LMSR vs CLOB、零 LP 风险 vs 库存风险、原生操作 vs Polygon 合约、保证金预言机 vs UMA、无 CTF。 | +| 88 | **Regulatory arbitrage** | 🏛 客户端 | 司法辖区客户端模型意味着每个地区在中立的 VIZ 之上构建其合规(或无许可)客户端。监管定位完全在客户端层。 | +| 89 | **Regulatory classification** | 🏛 客户端 | 市场是 swaps/博彩/证券由各辖区在客户端层决定;协议分类中立(无许可与受监管客户端用同样的 `pm_*` 操作)。 | + +## 摘要 —— Onix-on-VIZ 设计究竟改变了什么 + +**结构性解决(核心优势):** LP 侧的逆向选择、毒性流、跳空风险、无常损失、做市商库存风险 → 全部消除,因为赢家 +只由输家被没收的本金资助、LP 本金无条件返还(CPMM 用 AM-GM 证明;LMSR 用同注分彩)。**LMSR 的二元失败** → +通过二元用 CPMM、将 LMSR 限于做市商非对手方的多元市场而规避。**流动性提供、最小可行流动性、长尾可行性** → +无风险 LP + 懒惰池自动分配。**预言机设计、争议解决、CVM** → 保证金预言机 + 14 项声誉 + 加权委员会争议。 +**价格发现、套利、条件代币** → 价格一致性是数学性的(`Σ price = 1`),故无需订单簿、拆/合或内部套利层。 + +**不需要 / 刻意省略:** 订单簿、CDA、买卖价差、语义最小变动价位、订单流套利、CTF 拆/合、组合/协方差/相关/相对 +价值/连串市场、UMA、对等预测、LOX。 + +**下放到司法辖区客户端层:** 联邦优先、监管分类/套利、选举市场合法性、KYC/内幕交易执法、市场监控。 + +**自规范以来新上线(HF14):** 可选批量拍卖 + 提交-揭示下注(二元)、可选杠杆子系统(头寸抵押)、懒惰池、 +`endogeneity_tier`、链上创建者封禁、逐下注者/杠杆结算虚拟操作 + 插件 API,以及 PM 争议与 DAO 提案中的懒惰池治 +理权重。 + +**在 VIZ 路线图(今日部分):** 共享/类别流动性池(解决碎片化)、自动数据预言机(→ 自裁定客观市场)、分布市场、 +AI 代理。*(注:争议的提交-揭示投票**不在**此列——它被刻意拒绝;争议听证保持公开。下注的提交-揭示已上线。)* + +**仍开放 / 行为性(已缓解,非消除):** 冷门偏差/Yes 偏差、靠深度的价格操纵、跨平台套利。反身性家族有具体缓解 +方案——见下。 + +## 反身性家族的缓解 {#fanshenxing-jiazu-de-huanjie} + +endogeneity、reflexivity、凯恩斯选美(KBC)与 hyperstition 是**同一根因在不同层**:市场/价格影响其所衡量的结 +果。一小套原语应对全部四者。 + +### 缓解原语 + +| 原语 | 状态 | 修复 | 备注 | +|-----------|--------|-------|---------| +| **提交-揭示争议投票** | **已拒绝(不会构建)** | KBC | 委员会争议是**公开听证**:`pm_dispute_vote` 是公开选票,**按设计保持公开**——DAO 信誉依赖透明裁决。投票可改至关闭;KBC 残余被接受(投票不因与多数一致而获酬)。 | +| **提交-揭示下注(批量)** | **上线(可选,二元)** | endogeneity、reflexivity、信息不对称 | `pm_commit_bet`/`pm_reveal_bet`/`pm_batch_settle` 隐藏在途订单方向/大小,使公开价格在下注窗口内不「泄露」。按统一价**批量**结算。 | +| **`endogeneity_tier` 字段** | **上线字段** | endogeneity | 预言机标 1 经济数据、2 体育/排定、3 政治/社会;UI 展示反身性风险;客户端可限制 tier-3。 | +| **外生裁定(自动数据预言机)** | 路线图(高) | endogeneity、reflexivity | 裁定绑定到市场无法影响的外部数据源 → 干净温度计。 | +| **禁止类别清单 + 创建者封禁** | **创建者封禁链上上线**;清单在客户端层 | 有害反身性、hyperstition | 封禁 YES 会制造「拥趸」的市场(暗杀/「悬赏」/恐怖、宣传市场)。经链上创建者封禁 + 客户端类别过滤。 | +| **深度无风险 LP 流动性** | 今日核心 | 操纵驱动的反身性 | 飞轮使订单深,故为「头条」操纵抬价代价高。 | + +### 净裁定变化 + +| 概念 | 之前 | 之后 | +|---------|--------|-------| +| Keynesian Beauty Contest | 🔴 开放 | ✅ 市场 / ⚪ 争议——投票**按设计**公开(无提交-揭示;可改选票、投票者不获酬、池内投票者有权) | +| Endogeneity | 🔴 开放 | 🟡 已缓解——`endogeneity_tier` + 提交-揭示/批量**上线**;外生预言机路线图 | +| Reflexivity | 🔴 开放 | 🟡 已缓解——创建者封禁**链上上线**;类别清单客户端层 | +| Hyperstition | 🔴 开放 | ➖ 按设计排除(可选客户端产品) | diff --git a/@l10n/zh-CN/docs/prediction-markets/early-exit-deferred-claim.md b/@l10n/zh-CN/docs/prediction-markets/early-exit-deferred-claim.md new file mode 100644 index 0000000000..478ef92304 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/early-exit-deferred-claim.md @@ -0,0 +1,118 @@ +# 提前退出的递延权益(F1 / #300) + +状态:设计已锁定(owner 2026-08-08),实现正在 `pm` 分支上进行。 + +## 问题 + +市场是一个混合体:入口和提前退出用 CPMM(二元)/ LMSR(多元)**曲线**, +而持有到裁定的仓位用**帕里姆图(pari-mutuel)**结算。任何穿越曲线的往返(先买后卖、在结算前) +都会把一笔交易盈亏兑现到曲线深度——即 LP——之上,与 Uniswap 的无常损失完全一样。但设计向 LP +承诺**本金保障**(只收费用、无无常损失)。这两者是矛盾的。 + +在二元市场上,有两条代码路径会逆着曲线退出: + +- **杠杆**(`liquidate_position` / `pm_leverage_close`):总是如此;在结算时被强平;被池贷款放大。 +- **普通撤单**(`cancel_bet`,F2 按曲线定价退还):发生在下注窗口内。 + +两者都把 `residual = stake − curve_refund` 路由到 `forfeit_pool`(带符号)。当一次提前退出 +*盈利*时(`curve_refund > stake`),`forfeit_pool` 变为**负值**。结算时 +`winners_pool = losers_sum − fees + forfeit_pool`;如果杠杆/提前退出的利润超过了输家的押注, +则 `winners_pool < 0`,被下限到 0,缺口(`uncovered`,F1)向 LP 本金收取——或者当 LP 本金耗尽时被 +铸币。这是可达成的:用一个遵守门槛的 CPMM 模拟证明了这一点(单向拉升,`uncovered = 7316`); +Babin 的重放语料在 1163/1988 对中命中它。 + +根本原因:**按曲线定价的退出支付的是一个不受输家池约束的 bonding-curve 价值**,而结算是帕里姆图。 +这个缺口落在了 LP 头上。 + +## 模型(已锁定) + +提前退出不再从 LP 身上抽取曲线价值。相反,退出记录一份**结果相关的递延权益**,在结算时由 +**输家池的有界份额**提供资金。 + +### 退出时记录 +`{ position_id, kind (bet|leverage), chosen_outcome, claim_amount, exit_time }`。 + +### 普通撤单 +- **本金立即、无条件返还**:`refund = min(curve_refund, stake)`(自己的钱,不是借来的)。撤单可以 + 止损或保本,但永远不会在撤单时刻兑现曲线利润。 +- **利润尾巴** `max(curve_refund − stake, 0)` → 对所选结果的一份递延权益。 +- **按深度归一化定价(审计 #1-C):** cap/tail 的拆分是按该下注*入场时*的曲线深度重新定价的,而不是 + 当前深度。`pm_bet_object.entry_liquidity` 记录 CPMM 下注击中曲线那一刻(即时 `place_bet` 和 + 批量填充)的 `liquidity_sum`。撤单时两个储备都按 `entry_liquidity / liquidity_sum` 缩放(在缩放后的 + 储备上做 mirror-of-buy)。因为下注保持 `k` 不变,而流动性操作把 `k` 缩放 `f²`、把 `liquidity_sum` + 缩放 `f`,所以 `sqrt(k_entry / k_now) == L_entry / L_now` **精确**成立 → 确定性、无需开方(无整数 + 开方)。结果被**钳制到真实的 `curve_refund`**,因此归一化只能*降低*赔付,永远不会抬高。这扼杀了 + 自我流动性通胀向量(下注 → 自己的 `add_liquidity` 吹大深度 → 更大的 `curve_refund` → 撤单铸出更大 + 的尾巴 → 提现整体拿回流动性 = 现金中性的保证利润),又不打开收缩侧的漏洞。当 `entry_liquidity` 缺失 + (该字段出现之前的旧下注)时回退到旧定价。 + +### 杠杆平仓 / 清算 +- **抵押品不单独返还**——它是池子的 first-loss margin。池子先从 `cv` 里收回自己的义务 + (`loan·(1+R) + funding`);如果 `cv < obligation`,抵押品补上缺口。 +- **剩余** `max(cv − obligation, 0)` → 对所选结果的一份递延权益。 +- 因此杠杆是一笔**带杠杆的方向性下注**,而不是波动率收割:只有当你的结果赢了、而且桶里有空间时才 + 盈利;押错结果就损失抵押品。 +- 注意:现在有两个不同的谓词:**偿付能力**(`cv ≥ obligation`,决定贷款回收)vs + **结果胜出**(决定索取权益的权利)。一个仓位可能偿付能力充足却押在输的结果上 → 池子被补全, + claim = 0。 + +### 结算 +1. `bucket = pm_early_exit_reward_cap_percent × losers_sum / 10000`(默认 33%)。 +2. 只收集**胜出结果**上的递延权益(输的结果上的权益 → 0)。 +3. 按 `exit_time` **FIFO**(先出先付)支付它们,直到桶被耗尽;没有 per-position 上限(owner + 2026-08-08:FIFO 顺序 + 总桶就是边界)。一份剩余桶无法全额支付的权益会被部分支付;其余部分不付 + (haircut)。 +4. **未使用的桶返回赢家池**——持有的胜出下注按帕里姆图分享它。 + +### 保证 +``` +paid_claims ≤ bucket = cap · losers_sum +winners_pool = losers_sum − fees − paid_claims + honest_forfeits + ≥ (1 − cap) · losers_sum − fees ≥ 0 (cap < 100%) +``` +- `uncovered` **在构造上不可能**;无铸币;**LP 本金永不被触及**;lazy pool 不承担杠杆无常损失。 +- **输的结果永远不盈利**(owner 要求)。 +- 持有的赢家获得输家池的 `≥ (1 − cap)`,外加任何未使用的桶。 +- 提前退出是一份**有界的、相关的折扣**(≤ cap,FIFO),相对于持有到裁定(全额帕里姆图份额)→ + 不存在针对持有的套利;这是一份刻意的流动性折扣。 + +## 链参数 + +`pm_early_exit_reward_cap_percent`(uint16,bp,默认 **3300** = `losers_sum` 的 33%)。 +验证者中位数投票参数;`validate()` 限定 `≤ 10000`。已加入 +`chain_properties_pm` + FC_REFLECT + `calc_median`(DONE,single-TU 验证)。 + +## 实现触点(节点) + +- [x] 链参数 `pm_early_exit_reward_cap_percent`(struct/validate/reflect/median)。 +- [ ] 对象 `pm_deferred_claim_object`(+ 按 market、按 exit_time 的索引)——space 30,追加到 + `object_type` 枚举末尾(snapshot-safe,如同 `pm_lazy_withdraw_request`)。 +- [ ] `cancel_bet`:返还 `min(curve_refund, stake)`,记录利润尾巴权益;停止把负 residual 路由到 + `forfeit_pool`。 +- [ ] `liquidate_position` / `pm_leverage_close`:池子取走 obligation,记录 `cv − obligation` + 权益,标记所选结果;去掉立即的 `bettor_received`;停止负 `forfeit_pool`。 +- [ ] 结算(`settle_market`):强平之后计算 `bucket`,按 exit_time FIFO 支付胜出结果的权益,剩余 → + 赢家池;移除 `uncovered`/F1 的 `settle_liquidity` 收取路径(LP 不再吸收它)。 +- [ ] 快照:把 `pm_deferred_claim_object` 加入 allowlist(+ import handler)。 +- [x] 虚拟操作 `pm_early_exit_claim_paid`(account, market, kind, outcome, `claimed`, `paid`)—— + 追加到 `operation` 变体末尾(op-id 保持稳定),做了 FC_REFLECT,在结算的分配循环里、紧挨着 + `adjust_balance` 发射,并路由到提前退出者的账户历史(`account_history` 的 impacted-accounts + visitor)。`claimed` vs `paid` 暴露任何桶耗尽导致的 haircut。单独的 `adjust_balance` 不留历史 + 痕迹——这补上了缺口。 +- [x] 读 API:`get_deferred_claims(market, [from=0], [limit=100])` —— 通过 `by_claim_market` + 的 FIFO 退出顺序;在已结算市场上为空(权益已消费)。仅插件(客户端通过 rawApi/JSON-RPC 调用), + 与 `get_lazy_withdraw_requests` 一致;不接钱包。 + +## 客户端 / 库 / 文档后续 +- viz-js-lib / viz-php-lib / viz-python-lib:v5 chain_properties_pm 里的新链参数(序列化 + lock-step,字节校验),任何新的读方法 / vop。 +- Forecaster:提示 + 操作说明(杠杆 = 方向性,提前退出 = 有界折扣),在仓位上展示待定的递延权益。 +- WebVIZWallet:同样的操作说明更新(如果展示)。 +- 学术文章:带数学的 `early-exit choice`(regular vs leverage-from-lazy-pool,验证者集合奖励上限)。 + +## 被否决的替代方案(原因) +- 把 `uncovered` 摊到所有 LP / 铸币(status quo)——破坏 LP 本金承诺。 +- 只本地化到 lazy pool —— 池子可能被耗尽;仍然只是近似;撤单会泄漏。 +- 在退出时封顶收益 —— 退出时刻你不知道 `losers_sum`;递延消除了这一点。 +- 完全 AMM 结算 —— 放弃 VIZ 的帕里姆图论点。 +结果相关的递延权益是唯一能在保持帕里姆图的同时给出 LP **硬性**保证的选项。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/active-lp.md b/@l10n/zh-CN/docs/prediction-markets/guides/active-lp.md new file mode 100644 index 0000000000..72ab1ee16c --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/active-lp.md @@ -0,0 +1,42 @@ +--- +title: "主动 LP —— 直接的市场流动性" +description: "直接 LP 为具体市场设定曲线深度,从费用与迟下注惩罚中获得收入,并且本金受保障:提取是价格中性的,settle_liquidity 无条件返还本金。杠杆不触及直接 LP。" +--- + +# 主动 LP:直接的市场流动性 + +你给市场深度:把 VIZ 放进某个具体市场的曲线,让下注不会太剧烈地推动价格。作为回报,你获得费用的一份。与「做市商即庄家」模型的关键区别:在 VIZ 上,直接流动性是**本金受保障的** —— 结果不可能让你亏进去。 + +## 两段话讲清 + +你的资金(`pm_add_liquidity`)进入市场的曲线,决定下注推动价格时有多平滑。池越深,大玩家越舒服,成交量越大 —— 因而费用也越多。你赚取**下注的一个百分比**(市场费用)外加**迟下注惩罚**(抗狙击惩罚)的一份,后者归 LP 累积。 + +你的本金不取决于谁获胜。提取(`pm_withdraw_liquidity`)是**价格中性**的:按比例收缩储备,把你的本金返还,而不推动曲线(一进一出的往返不改变价格)。而在市场结算时,`settle_liquidity` **无条件**返还每位 LP 的本金,再加上奖金(费用、未分配给赢家的余额、惩罚池)。LP = 本金保障 + 费用收入 —— 而费用收入**只在结算时**实现。 + +## 逐步发生了什么 + +**你添加流动性。** `pm_add_liquidity` —— VIZ 进入市场的曲线,你按出资比例获得池份额。你的存入不会移动价格(你是对称添加的)。 + +**市场开放期间。** 下注会被扣一笔费用,并按份额分配给 LP。迟下注支付抗狙击惩罚,同样归流动性一方。你的收入随周转量增长而累积。 + +**你提取(可选)。** `pm_withdraw_liquidity`,部分或全部。提取是价格中性的:你取回**本金**(费用收入只在结算时实现——见下文),而曲线不发生位移。不存在经典 AMM 里的无常损失:一进一出的往返既不移动价格,也不吃掉你的资金。 + +**结算。** 市场一旦裁定,流动性会**自行**通过逐区块结算返还。`settle_liquidity` 无条件把本金交给每位 LP,再加奖金。流动性从下注结束起锁定至结算(在赔付计算期间),随后释放。 + +## 主动 LP 需要理解什么 + +- **本金保障指的是结果,而不是天底下的一切。** 你不会因为市场里谁获胜而损失本金。你的收入是费用与惩罚;结果风险不会被转移到你身上。 +- **你不是杠杆的对手方。** 杠杆交易者的贷款由**懒惰池**(一个被动产品)垫付,而不是由你的直接流动性垫付。杠杆不触及你的本金。(关于覆盖杠杆超额利润有一个微妙的设计细节 —— 它被局部化到池,而不是直接 LP。) +- **深度 = 成交量 = 收入。** 流动性太薄会吓退大额下注者;你的出资直接影响市场质量,并通过成交量影响你的费用。 +- **结算期间的锁定是正常的。** 从下注结束到赔付被算出,流动性处于锁定状态;那是结算流程,不是损失。之后本金就会返还。 +- **收入在结算时实现。** 已赚取的费用(earned_fee)在市场裁定时才最终确定;裁定之前它们会被体现出来,但要在返还中记账。 +- **提前提取只拿回本金。** `pm_withdraw_liquidity` 要求行处于活动状态(`status == 0`),而费用计数器是在结算时与 `status = 3` 一起赋值的——因此提前提取无法观察到任何佣金;你只是价格中性地把本金拿回来。没有可以中途领取的“当前已赚收入”。 +- **void 市场不支付收入。** 如果市场以 void 结束(无竞争者——预言机没有给出赢家,例如来源被取消或 50/50),下注会退款,LP 只拿回本金,**零**奖金。LP 收入只来自裁定出赢家的市场。 + +## 与你相邻的角色 + +- **市场创建者** —— 提供初始流动性(他们也是直接 LP)并设定费用。 +- **下注者** —— 支付构成你收入的那笔费用。 +- **被动 LP(懒惰池)** —— 另一种产品:垫付杠杆的被动资金;不要与直接的市场流动性混淆。 + +延伸阅读:《被动 LP(懒惰池)》(有何不同、杠杆风险落在哪里)、《为何是池而不是赔率》(曲线如何运作)、《市场创建者》(费用与深度如何设定)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/bettor.md b/@l10n/zh-CN/docs/prediction-markets/guides/bettor.md new file mode 100644 index 0000000000..16efb6314a --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/bettor.md @@ -0,0 +1,42 @@ +--- +title: "下注者 —— 如何在 VIZ 预测市场下注" +description: "下注进入共享的市场池并推动浮动赔率;奖池按份额比例分给判对的人,裁定时自动赔付。" +--- + +# 下注者:如何在 VIZ 预测市场下注 + +你来是为了对某个结果下注 —— 「是/否」、「谁会赢」、「事件会不会发生」。在 VIZ 上这不像有固定赔率的博彩公司那样运作,而是一个**共享池**。我们来走一遍:你的钱究竟经历了什么,赢利又是如何形成的。 + +## 两段话讲清 + +你把 VIZ 押在其中一个结果上。你的下注进入**市场池**并推动价格:某个结果上押得越多,它就越贵,下一枚代币能换到的「份额」(权重)就越少。赔率不会在下注那一刻被锁定 —— 它是**浮动的**,由此刻池的平衡决定。 + +当预言机宣布结果时,整个池按份额比例分给判对的人。判错就输掉下注 —— 它归赢家。判对就取走你在奖池中的那一份。没有「锁定 2.5,现在等着」这回事:赔率就是你看到的当前价格,并随着他人下注而变化。 + +## 分步会发生什么 + +**下注。** 你选择一个结果和一个金额。节点沿池曲线计算这个金额在当前价格下应得多少份额(权重),并记录持仓。金额进入池,该结果的价格上移。 + +**市场开放期间。** 价格是活的:其他人下注,赔率浮动。你可以继续加注;在允许撤注的市场上,你可以在关闭前**撤销**下注(`pm_cancel_bet`)。要注意:撤注是把你的持仓按当前曲线卖回,而不是退还名义金额。如果市场自你下注以来已经移动,你拿回的会少于你投入的 —— 这不是惩罚,而是公平的退出价格;你永远不会拿到超过你的下注额(而若价格朝对你有利的方向移动,沿曲线产生的盈余会变成一份取决于结果的递延索取权,而不是被交给市场)。详情 —— 「[撤注](./cancel-bet)」。 + +**下注关闭。** 到 `betting_expiration` 时不再接受下注。之后就是等待预言机给出结果。 + +**裁定。** 预言机宣布获胜结果(`pm_resolve_market`)。对判对者的赔付是**自动**且虚拟的:没有需要按的「领取」按钮,赢利在裁定时记入你的余额。失败的结果被清零,其资金进入赢家的奖池。 + +**如果没有结果。** 事件被取消或来源消失 —— 预言机宣告 **no-contest**(`pm_no_contest`):下注被退还,无人赢也无人输。而如果预言机一直沉默到截止期限,市场会作为**错过裁定(missed-resolution)**被作废 —— 你的下注同样会被退还(且预言机为此受罚)。无论哪种情况,你的钱都不会「烧掉」。 + +## 下注者需要理解什么 + +- **赔率不是固定的。** 你看到的价格是此刻池的状态。对不受欢迎的结果早下注能买到更多份额(你入场更便宜);等人群涌入,价格就不同了。详情 —— 见文章「为何是池而非赔率」。 +- **赔付按份额成比例,而非「下注额 × 赔率」。** 你和其他赢家按持仓权重分享奖池。最终倍数取决于下注最终如何分布在各结果上。 +- **无需手动领取。** 赢利与退款在裁定/撤销时自动到账。 +- **你可以挑战预言机。** 如果结果宣布有误,可以在争议窗口内提出争议(`pm_dispute_create`)—— 见关于争议的文章。 +- **流动余额。** 下注需要自由的 VIZ;已质押的 SHARES 不计入。如果不足,钱包会提示你。 + +## 你身边的角色 + +- **预言机** —— 宣布结果、并以声誉和保险保证金为此负责的人。 +- **流动性提供者** —— 其资本决定曲线深度的人(让你的下注不会过于剧烈地推动价格)。 +- **杠杆交易者** —— 用池的贷款对**价格**下注,而不是对结果下注;这是另一种工具。 + +接下来按口味选择:「为何是池而非赔率」(价格机制)、「预言机与裁定」(谁宣布结果、如何宣布)、「争议」(如何挑战)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/cancel-bet.md b/@l10n/zh-CN/docs/prediction-markets/guides/cancel-bet.md new file mode 100644 index 0000000000..fac1cebc6c --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/cancel-bet.md @@ -0,0 +1,37 @@ +--- +title: "取消下注 —— 按曲线价格退出" +description: "pm_cancel_bet 按当前曲线价格(curve-priced)把持仓卖回,而不是按面值退款。你收到的金额永远不会超过你的下注额(cap-at-stake):亏损的差额进入市场的 forfeit_pool,而盈利的超出部分则成为依赖结果的递延债权。只要市场允许,在下注结束之前都可以取消。" +--- + +# 取消下注:按曲线价格退出 + +在市场关闭之前改主意了?在允许的市场上,一笔下注可以被取消(`pm_cancel_bet`)。但有一点很重要:这**不是**「把我的钱还给我」,而是按当前价格把持仓卖回去。下面看看能拿回多少、以及为什么。 + +## 两段话讲清 + +取消是按**当前曲线**(curve-priced)退出,而不是按面值退款。你按取消那一刻已经形成的价格卖出自己的份额(`weight`)。如果市场自你下注以来已经移动,你拿回的会**少于**你投入的 —— 这是公平的退出价格,不是罚则。反过来也一样:你收到的**永远不会超过你的下注额**(cap-at-stake)—— 曲线上的盈利无法在取消那一刻套现。 + +差额去向取决于它的方向。如果曲线支付的**少于**下注额(价格朝不利于你的方向移动)→ 这笔缺额(`curve_residual`)**留在市场里**,进入 `forfeit_pool`,并在结算时分配给赢家。如果算出来**高于**下注额(价格朝对你有利的方向移动)→ 这笔超出部分既不给市场,也不立即支付:它成为你的**依赖结果的递延债权**,并在你的结果获胜时于结算时兑付(详见「[提前退出与递延债权](./early-exit)」)。这一过程中不会铸造也不会销毁任何代币。只有在下注结束(`betting_expiration`)之前、并且市场在创建时允许取消的情况下,你才能取消。 + +## 逐步运作 + +**按曲线卖出。** 节点按池当前的储备计算你的份额值多少,并返还该金额。价格就是每笔下注都在推动的那同一条曲线;你的退出同样会把它稍稍推回去一点。 + +**Cap-at-stake(以下注额为上限)。** 即时返还的金额上界是你原始的下注额。即便你所选结果的价格已经上涨、「在曲线上」这些份额更值钱,你拿回的也不会超过你投入的。曲线上的超出部分并没有丢失:它被结转为一笔**递延债权**,并在你的结果获胜时于裁定时兑付。取消是一次退出,而不是即时获利的方式。 + +**Residual → forfeit_pool。** 如果曲线支付的少于面值,那笔差额(`curve_residual`)不会消失于无处:它被路由进市场的 `forfeit_pool`,并在结算时归于赢家。代币守恒 —— 这是维持守恒性的构造的一部分(没有「烧掉」,也没有静默铸造)。 + +## 需要理解的地方 + +- **返还 ≠ 面值。** 如果市场自你下注以来已经移动,你拿回的会更少。那是流动性的代价,不是惩罚。 +- **取消无法即时获利。** Cap-at-stake:你最多收到你自己的下注额。曲线上的盈利会成为一笔递延债权,并在裁定时、在你的结果获胜的情况下到账。 +- **亏损的差额留在市场里。** 如果退出支付的少于下注额,这笔缺额经由 forfeit_pool 归于赢家,而不是「凭空消失」。反过来,盈利的超出部分则作为递延债权为你锁定。 +- **只在关闭之前、只在允许之处。** `betting_expiration` 之后没有取消;在没有取消开关的市场上同样没有。 +- **杠杆的平仓方式不同。** 退出一个杠杆持仓(`pm_leverage_close`)有它自己的机制(贷款、cancel_value);那里的残余同样被路由进 forfeit_pool。见[杠杆交易者](./leverage-trader)。 + +## 相关阅读 + +- [下注者](./bettor) —— 一笔下注的生命周期,取消是其中的一条路径。 +- [提前退出与递延债权](./early-exit) —— 取消中盈利的那条尾巴去了哪里。 +- [为何用池而非赔率](./why-pool-not-odds) —— 为什么价格(以及退出价格)会浮动。 +- [规范](../specification) —— curve-priced 取消的公式与残余的路由。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/commit-reveal.md b/@l10n/zh-CN/docs/prediction-markets/guides/commit-reveal.md new file mode 100644 index 0000000000..9b7417a06b --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/commit-reveal.md @@ -0,0 +1,44 @@ +--- +title: "隐藏下注 —— 提交-揭示与批量" +description: "隐藏下注:先提交(托管,不公开结果/金额),然后在窗口内揭示。揭示之前的私密性保护你免于被抄袭和被狙击。未能揭示则部分托管被没收进 forfeit_pool。需要一个带 allow_batch 的市场。" +--- + +# 隐藏下注:提交-揭示与批量 + +有时候,你的下注**不被提前看到**是很要紧的 —— 这样它就无法被抄袭,也无法被针对性地博弈。两阶段的隐藏下注就是为此而生:先把它「封起来」(提交),然后再揭示(揭示)。下面看它为什么存在,以及如果你不揭示会发生什么。 + +## 两段话讲清 + +一笔普通下注在链上立刻可见 —— 结果、金额、时间。隐藏下注把这些遮住直到揭示:你押上**托管**并以提交的形式记下这笔下注(`pm_commit_bet`),而不显示押的是哪个结果、押了多少。之后,在**揭示窗口**内你把它揭示出来(`pm_reveal_bet`),它便像任何其他下注一样进入彩池。在揭示之前没人知道你的持仓 —— 这就是对抄袭、抢跑以及狙击大额下注的防护。 + +私密性伴随着责任:如果你在窗口内**未能揭示**这笔下注,**没收**(forfeit)就会启动 —— 部分托管(罚则部分)进入市场的 `forfeit_pool`(归赢家),其余退还。隐藏下注只在启用了批量模式(`allow_batch`)的市场上有效,并且要求一个最低托管额(`pm_min_batch_bet`)。 + +## 逐步运作 + +**提交。** `pm_commit_bet`:你押上托管(≥ `pm_min_batch_bet`,量级为 1 VIZ),并以密封形式记下这笔下注。链上显示你押了些什么,但不显示结果与金额。托管被扣划。 + +**揭示窗口。** 你会得到一个有限的窗口(纪元 + `pm_reveal_window_blocks`,量级为几分钟)。你必须在其中揭示。 + +**揭示。** `pm_reveal_bet`:你公开结果与金额,下注按当前价格进入彩池 —— 从此它就是一笔普通下注。为了揭示,客户端需要你那笔提交的 `commit_id`(`get_account_commits`)。 + +**未揭示 → 没收。** 错过窗口,没收就自动发生:罚则(托管的一部分,由 `no_reveal_fee_percent` 设定)进入市场的 `forfeit_pool`,余额退还。这就是占了一个位置却没把下注完成的代价。 + +## 为什么存在 + +- **意图的私密性。** 大玩家不希望自己的下注在进入彩池之前被抄袭或被针对性地博弈。 +- **反狙击。** 隐藏阶段让偷看并抢跑他人的下注变得更难。 +- **批量。** 提交被收集起来并成批揭示 —— 这是一种更公平、更私密的下注轮次模式。 + +## 你需要理解的地方 + +- **两个阶段,两个动作。** 提交与揭示是不同的操作,中间夹着一个窗口。忘了揭示,你就损失掉罚则部分。 +- **揭示是强制的。** 没收不是 bug,而是把开了头的事做完的激励;罚则通过 forfeit_pool 归于赢家。 +- **仅在 allow_batch 市场上。** 并非每个市场都支持隐藏下注。 +- **你需要 commit_id。** 为了揭示,客户端会取出你未结的提交(`get_account_commits`)—— 没有它就没有可揭示的东西。 +- **终止开关。** 整个提交-揭示是一个带中位数投票开关(`pm_commit_reveal_enabled`)的子系统;验证者无需硬分叉即可停用它。 + +## 相关 + +- [下注者](./bettor) —— 作为对比的普通(可见)下注。 +- [为何是彩池而非赔率](./why-pool-not-odds) —— 一笔被揭示的下注落到哪里。 +- [规范](../specification) —— 揭示窗口、`no_reveal_fee_percent`、批量机制。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/coupon.md b/@l10n/zh-CN/docs/prediction-markets/guides/coupon.md new file mode 100644 index 0000000000..10181c5a0f --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/coupon.md @@ -0,0 +1,40 @@ +--- +title: "投注单 —— 一笔交易里的多笔下注" +description: "投注单把 N 笔下注(pm_place_bet)打包进一笔已签名的 VIZ 交易。这笔交易是原子的:任一注无效就会让整张投注单被拒绝,不存在只成交一半的状态。这不是串关 —— 各注彼此独立,赔付不会相乘。" +--- + +# 投注单:一笔交易里的多笔下注 + +投注单是一种一次性对多个结果下注、而不必逐笔单独签名的方式。你点击结果来收集盘口,设置金额,然后**用一笔交易**把一切发送出去。 + +## 两段话讲清 + +技术上,投注单就是一笔已签名的 VIZ 交易里的 N 个 `pm_place_bet` 操作。链上交易在**写入层面是原子的**:要么整笔生效,要么整笔被拒。所以只要有一注无效 —— 市场已经关闭、余额不足以覆盖所有注的总额、滑点保护被触发 —— **整张投注单都会失败**。不存在「一半下注成交了」这种状态,你也永远不必去理清一个只成了一部分的结果。 + +同时,投注单**不是串关(也不是累积过关)**。每一注都是投进各自市场奖池的一笔普通独立下注,有自己的赔付。赔率不会相乘:某一注的奖金并不取决于其他注是否命中。投注单省下的是签名,并让一组下注同时发生 —— 但它不会构成「所有结果都必须命中」的组合。带相乘的真正组合是一个单独的协议原语,在下注这一层目前还不存在。 + +## 逐步运作 + +**收集。** 在事件列表与事件页面(「盘口」视图)中,点击一个结果就会把一注加入投注单:市场、结果、金额(默认 1 Ƶ)以及你的滑点保护。再次点击同一个结果会更新这一注,而不是产生重复项。投注单存放在本地浏览器中 —— 在发送之前,它完全不触碰链。 + +**发送前的检查。** 昨天攒下的投注单里,可能还留着一个下注已经关闭的市场。链会拒绝这样一注 —— 连整笔交易一起拒绝,所以客户端会在打开投注单时用与节点相同的规则检查每一注(市场处于活跃状态,且要么是无固定期限的,要么其截止时点尚未过去),把失效的标记出来,并在它们被移除之前不允许发送。流动余额单独检查,针对的是**所有注的总额**:奖池只接受用自由 VIZ 下注,已质押的份额不计入。 + +**发送。** 所有注用同一个密钥(active)签名,并作为一笔交易发出。这里没有网络手续费 —— 上限由账户的能量决定,而按这个尺度衡量,一笔包含 N 注的交易比 N 笔单独交易更便宜。 + +**之后会发生什么。** 一旦写入,每一注就各自过自己的日子:在各自市场的奖池里有自己的份额、自己的裁定、自己的自动赔付。在「我的活动」中,它们显示为普通的下注。 + +## 需要理解的地方 + +- **全有或全无 —— 是在写入层面,而不是在结果层面。** 投注单的原子性说的是落入区块,而不是押中与否。一注输了 —— 其余各注照样计算、照样赔付。 +- **赔付不会相乘。** 这不是串关。如果你想要更大的风险,就提高某一注的金额,而不是增加注的数量。 +- **一注失效就毁掉整次提交。** 市场已关闭、自由 VIZ 不够所有注的总额、最少份额保护被触发 —— 链就会拒绝整张投注单。客户端会提前把这样的注高亮出来。 +- **隐藏下注不进投注单。** 提交-揭示与批量模式是有各自揭示窗口的单独路径;只有普通的(「即时」)下注才会被收进投注单。 +- **投注单存在浏览器里。** 在你发送之前,它是你设备上的一份草稿:链对它一无所知,换一台设备也不会有它。 + +## 相关阅读 + +- [下注者](./bettor) —— 投注单由之构成的那一笔下注的生命周期。 +- [为何用池而非赔率](./why-pool-not-odds) —— 为什么一注的价格会浮动,以及最少份额保护是做什么用的。 +- [事件与元数据](./events-metadata) —— 一场比赛的各条盘口如何被聚合成一张事件卡片,而那里正是构建投注单的便利之处。 +- [隐藏下注(提交-揭示)](./commit-reveal) —— 不属于投注单的那条路径,以及为什么。 +- [`pm_*` 操作](../../protocol/operations/prediction-markets) —— 交易由之构建起来的 `pm_place_bet` 操作。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/disputes.md b/@l10n/zh-CN/docs/prediction-markets/guides/disputes.md new file mode 100644 index 0000000000..e0ab2b88c2 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/disputes.md @@ -0,0 +1,44 @@ +--- +title: "争议 —— 如何质疑一个结果" +description: "若预言机宣布了错误的结果,可在宽限窗口内对其提出质疑(pm_dispute_create,保证金费用)。争议由一次投票裁决(委员会模式或账户模式);裁定有误会处罚预言机,而提出者要押上自己的保证金。赔付可能被重算。" +--- + +# 争议:如何质疑一个结果 + +预言机是人或服务,它可能出错,也可能作弊。争议就是那道安全锁:参与者用它在赔付还能被重算的时候质疑已宣布的结果。下面看它如何运作,以及各方各自押着什么。 + +## 两段话讲清 + +预言机宣布结果(`pm_resolve_market`)之后,**争议窗口**开启(宽限期,`pm_dispute_grace_sec`,量级为 12 小时)。在该窗口内,任何参与者都可以通过缴纳一笔**保证金费用**(约 1000 VIZ)来提出争议(`pm_dispute_create`)—— 这是让网络复核结果的价格,也是对空洞质疑刷屏的防护。 + +争议由一次**投票**裁决(`pm_dispute_vote`,使用 `active` 密钥)—— 采用市场创建者设定的模式之一:委员会(受信任的仲裁人)或账户投票。若争议认定裁定有误,结果被更正、赔付被重算、**预言机受罚**(保证金/声誉),提出者获得奖励。若认定裁定正确,提出者失去保证金。双方都有钱押在上面,因此质疑是就事论事提出的,而不是随手乱开。 + +## 逐步运作 + +**裁定与窗口开始。** 预言机宣布了结果 → 赔付已入账 → 争议宽限期开始计时。计时进行期间,结果并非最终。 + +**提出争议。** `pm_dispute_create` 附带保证金费用。市场进入争议状态(赔付被延后/被标记),并出现一条争议记录,写明预言机与发起人。发出 `pm_dispute_opened` 虚拟操作(vop)(它会落入预言机与提出者双方的历史)。 + +**投票。** 该模式的参与者以 `pm_dispute_vote`(`active` 密钥)投票。模式及其成员在市场创建时设定(委员会或账户)。投票收集至响应/裁决截止时点。 + +**终结。** 争议按投票结果关闭(`pm_dispute_finalize`),或在预言机未及时响应时自动关闭(`pm_dispute_auto_close`)。结果: +- **裁定有误** → 结果被更改,按正确结果重算赔付,预言机受罚,提出者获得奖励。 +- **裁定正确** → 结果维持,提出者失去保证金,预言机的声誉得到确认。 + +**对赔付的影响。** 争议进行期间,被质疑市场上的最终赔付不视为最终。终结之后,按已确认的结果进行结算。 + +## 需要理解的地方 + +- **窗口是有限的。** 只有在宣布之后的宽限期内才可能提出质疑。错过则结果为最终。在提前裁定(`allow_early_resolution`)时,窗口会收缩到裁定发生的那一刻,但提出者仍然保有自宣布起完整的 `pm_dispute_grace_sec`。 +- **保证金是双向切割的。** 提出者押上费用,预言机押上保证金与声誉。这让说谎的争议代价昂贵,而讲真话的代价低廉。 +- **模式由创建者设定。** 委员会模式更快、更可预期(受信任的仲裁人);账户模式更去中心化。下大注之前先看一下某个市场的模式。 +- **投票使用 `active` 密钥。** 参与者的大多数 PM 操作也是如此(`pm_dispute_vote`、`pm_dispute_create`)。 +- **争议是最后手段。** 对于 `reliability_score` 良好的诚实预言机,争议很少发生;系统性地输掉争议会拖垮它的声誉与保险。 + +## 相邻的角色 + +- [预言机](./oracle) —— 其裁定正被质疑的一方;押着保证金与声誉。 +- [下注者](./bettor) —— 若结果被错误宣布,可以发起争议。 +- [市场创建者](./market-creator) —— 在创建时设定争议模式与窗口。 + +关于本主题的更多内容:[预言机](./oracle)(因何受罚、声誉如何计算)、[下注者](./bettor)(一笔下注的生命周期)、[规范](../specification)(争议的形式化规则与截止时点)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/early-exit.md b/@l10n/zh-CN/docs/prediction-markets/guides/early-exit.md new file mode 100644 index 0000000000..1223054759 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/early-exit.md @@ -0,0 +1,43 @@ +--- +title: "提前退出与递延债权" +description: "提前退出(取消下注,或按曲线价格平掉杠杆)的盈利不会立即支付:它被记为依赖结果的递延债权,并在结算时从输家池中一份受限的份额里兑付。本金立刻返还,LP 保持受保护。" +--- + +# 提前退出与递延债权 + +VIZ 市场是一种混合体:入场与提前退出走**曲线**(如同在 AMM 上),而持有到最后的持仓则按**同注分彩**结算(一个共同的池)。正因如此,「提前退出的盈利」被安排得比看上去更精细。下面看看为什么提前退出的奖金不会立即到账。 + +## 两段话讲清 + +当你提前退出时 —— 取消下注(`pm_cancel_bet`)或平掉杠杆(`pm_leverage_close`)—— 你持仓的曲线价格可能会**高于**你的下注额。但这笔盈利现在无法支付:它没有输家作为后盾(市场还没有裁定),而如果从曲线里支付,缺额就会落到流动性提供者身上 —— 而在 VIZ 上,LP 是**本金保障**的。这就是为什么提前退出中盈利的那条「尾巴」不会当场套现。 + +取而代之的是,系统**立即且无条件地**返还你的**本金**(你自己的钱,且永不超过下注额),并把盈利的尾巴记为一笔**依赖结果的递延债权**。它在**结算时**从**输家池中一份受限的份额**里兑付 —— 也就是说,只有在你所选的结果获胜、并且彩池里有东西可付的情况下。这样既保持了代币守恒,也避免让 LP 去补贴交易者。 + +## 逐步运作 + +**本金 —— 立即到账。** 取消/平仓会立刻返还 `min(曲线价格, 你的下注额)`。提前退出可以砍掉亏损或让你平手出局,但它**不会**在退出那一刻实现盈利。 + +**盈利的尾巴 —— 转为递延债权。** 差额 `max(曲线价格 − 下注额, 0)` 被记为一笔针对**你所选结果**的债权。不是现金,而是「如果这个结果获胜,我们在结算时给你补上」。 + +**从一个有界的池中兑付。** 裁定时,该债权从**输家池中一份受限的份额**里支付(不是从曲线,也不是从 LP 本金)。如果背后没有输家的钱,赔付就会被削减。不会有静默铸造。 + +**公平的入场价格(防博弈保护)。** 「本金/尾巴」的拆分是按**你下注那一刻**的曲线深度、而不是按当前深度重新计算的。这堵住了「先下一注 → 自己加流动性 → 把深度抬起来 → 提走更大的尾巴」这一招:归一化只能减少赔付,永远不能增加。 + +## 为何这样设计 + +- **LP 被承诺了本金保障。** 如果提前退出的盈利从曲线里支付,那么缺额(当盈利跑过输家的下注额时)就会被记在 LP 本金上,或者被铸造出来 —— 两者都会破坏这项保证。递延债权把赔付挪到结算时,而在那里来源是输家,不是 LP。 +- **同注分彩只由输家作为后盾。** 赢家的盈利就是别人的亏损。在裁定之前还不存在「输家」—— 所以现在没有可以用来支付这笔盈利的来源。 + +## 需要理解的地方 + +- **提前退出取回的是本金,不是盈利。** 盈利的尾巴要等裁定。 +- **尾巴取决于结果。** 如果你所选的结果落败,就没有尾巴;如果它获胜,尾巴会从彩池里兑付(以可用的额度为限)。 +- **这保护的是 LP 与守恒性。** 这套机制是刻意为之,不是钱包的限制。 +- **杠杆遵循同一原则。** 杠杆的盈利同样在裁定之后、从一个有界的池中到账。见[杠杆交易者](./leverage-trader)。 + +## 相关阅读 + +- [取消下注](./cancel-bet) —— 提前退出的返还金额如何计算。 +- [杠杆交易者](./leverage-trader) —— 为什么杠杆的盈利被递延。 +- [主动 LP](./active-lp) —— 这项保护是为谁提供的。 +- [规范](../specification) —— 递延债权与受限份额的形式化模型。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/events-metadata.md b/@l10n/zh-CN/docs/prediction-markets/guides/events-metadata.md new file mode 100644 index 0000000000..ed1d99e35b --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/events-metadata.md @@ -0,0 +1,107 @@ +--- +title: "事件与元数据 —— 客户端如何把市场聚合成比赛" +description: "Forecaster 如何把市场聚合成「事件」:event / event_title / child 元数据键、父市场与子市场、节点索引了什么,以及如何创建一个能被客户端识别为事件的市场。" +--- + +# 事件与元数据:市场如何被聚合成比赛 + +现实世界中的一场比赛,通常是若干个市场:「谁获胜」、「击杀总数」、「首个 Roshan」。 +在链上它们是彼此独立的 `pm_market_object`,但客户端(Forecaster)把它们显示为一张事件 +卡片,带有对结果的下注和一个「还有 N 条盘口」的链接。这种聚合**只通过元数据**发生 —— +共识中不存在特殊的「父」对象,而这是刻意为之:协议保持 +最小化,而聚合关系由市场创建者在创建时设定。 + +## 简短模型 + +- 每个市场都带有一个自由格式的文本字段 `metadata`(`pm_create_market` 中的一个 JSON 字符串)。 +- 节点从其中解析出一份**键的白名单**并建立索引;其余一切都被忽略。 +- 具有相同 `event` 的市场就是「同一个事件」。没有 `child` 的市场是前台(父)市场, + 带 `child: 1` 的市场是子盘口(特殊玩法)。 +- Forecaster:「事件」标签页按 `event` 聚合活跃市场,把胜负盘市场显示为 + 前台市场,在通用列表中隐藏子市场,并在事件页面上把它们展示出来。 + +## 节点会索引的元数据键 + +节点只从 `metadata` 中提取以下字段(其他键不会被索引,但仍保留在 +原始 JSON 中 —— 客户端可以自行读取): + +| 键 | 类型 | 用途 | +|------|-----|-------| +| `title` | string | 市场的人类可读问题(卡片标题)。 | +| `category` | string | 列表分区(`esports`、`sports`、`crypto`…)—— `by_category` 索引。 | +| `subcategory` | string | 对分区的细化(可选)。 | +| `tags` | array 或 CSV | 用于筛选的标签;客户端读取的是**数组** `market.metadata.tags`,它由节点自行重建。 | +| `image` | string (URL) | 卡片封面(一个链接,不在链上托管)。 | +| `description` | string | 简短的裁定规则 —— 「预言机将如何判定结果」。 | +| `event` | string (slug) | **事件聚合键。** 同一场比赛的所有市场设置相同的 `event`。 | +| `event_title` | string | 事件的人类可读名称(「Dota 2: MOUZ vs Vici — TI 2026」)。 | +| `child` | 1 / true | **一条子盘口(特殊玩法)。** 从分类/标签列表中隐藏;在事件页面上可见。 | +| `banned_jurisdictions` | array 或 CSV | 供客户端使用的司法辖区过滤器。 | +| `condition_id` | string | 来源的去重标识符(供镜像解析器使用)。 | + +解析规则:`metadata` 必须是一个有效的 JSON 对象(非 JSON 的内容根本不会被索引); +`tags`/`banned_jurisdictions` 既接受数组也接受 CSV 字符串;`child` 接受 +`true`、`1` 或 `"1"`。标签匹配不区分大小写。 + +## 如何指定「父」市场 + +父市场不是显式指定的 —— 它是**由 `child` 的缺失推导出来的**: + +1. 给这场比赛的所有市场相同的 `event`(一个稳定的 slug:拉丁字母、连字符 —— + 例如 `dota2-mouz-vg-2026-07-12`)以及相同的 `event_title`。 +2. 对这场比赛的主市场(「谁获胜」/ 胜负盘)—— **不要设置** `child`。它就是 + 父市场:它在所有列表中保持可见,并成为事件卡片的门面。 +3. 对其余所有盘口(大小盘、让分盘、特殊市场)—— `child: 1`。它们会从 + 通用列表中消失(分类里没有噪音),但在事件页面上以及通过直接链接仍然完全可用。 + +同一场比赛三个市场的最小 `metadata` 示例: + +```json +// Parent (winner line) — WITHOUT child +{"title":"Will MOUZ beat Vici Gaming?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming"} + +// Child line 1 +{"title":"Total kills over 45.5 (map 1)?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming","child":1} + +// Child line 2 +{"title":"First Roshan — MOUZ?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming","child":1} +``` + +重要:`event` **在实践中是不可变的** —— 客户端按字符串精确匹配来聚合,所以要在 +创建市场之前就把这个键定好,并在这场比赛的所有盘口上完全一致地使用它 +(大小写与连字符都算)。 + +## 节点做了什么 + +- 用解析出的字段构建市场的元对象(`pm_market_meta`)并建立索引:按分类、 + 按标签,以及**按事件**(`by_meta_event`);元数据会进入快照。 +- `list_markets_by_category(...)` 默认**隐藏子市场**(`hide_children = true`, + 第 8 个参数)—— 列表只显示父市场;传 `false` 可以看到所有盘口。 +- `list_markets_by_event(event, from, limit)` 返回该事件的**全部**市场 —— 父市场与 + 子市场,没有过滤。这是事件页面的 API。 +- 在列表行中 `event_title` 在顶层返回,在完整市场卡片中则在 + `metadata` 内部;`tags` 由节点重建为数组。 + +## Forecaster 拿它做什么 + +- **「事件」标签页**(体育博彩视图):活跃市场按 `event` 聚合;前台市场 + 是标题看起来像胜负盘的那一个(`winner` / `moneyline` / `to win`),否则取 + 第一个二元市场;卡片显示带当前赔率的各个结果(点一下就把一注加入投注单),以及 + 一个「还有 N 条盘口」的链接。 +- **事件页面** `#/event/` —— 这场比赛的所有盘口在一个列表里(`list_markets_by_event`)。 +- **卡片**在问题上方显示 `event_title`;「分类 › 标签」面包屑通向各个列表。 +- **分类/标签列表**不显示子市场 —— 特殊玩法藏在事件卡片背后。 + +对创建者而言的实用要点:正确设置 `event`/`event_title`/`child`,就是 +「十个散落的市场在列表里制造噪音」与「一张整洁的比赛卡片,所有盘口都在 +里面」之间的区别。事件键弄错了,盘口就不会聚合;忘了 `child`,特殊玩法就会 +把通用列表塞满。 + +## 另请参阅 + +- [市场创建者](./market-creator) —— 启动流动性、预言机、费用。 +- [多结果市场](./multi-outcome) —— 什么时候一个 LMSR 市场胜过若干个二元市场。 +- [规范](../specification) —— 对象与索引的形式化模型。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/index.md b/@l10n/zh-CN/docs/prediction-markets/guides/index.md new file mode 100644 index 0000000000..3a047de344 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/index.md @@ -0,0 +1,44 @@ +--- +title: "预测市场指南 —— 按角色与按功能" +description: "面向 VIZ(Onix)预测市场参与者的解读文章:用通俗语言讲清每个角色(下注者、市场创建者、预言机、LP、杠杆交易者)与每项机制,并附向形式化规范的链接。" +--- + +# 预测市场指南(Onix) + +面向**参与者**的解读文章 —— 这不是规范,而是「这件事对你来说如何运作」。每个角色都从参与者的视角讲解:一个简 +短模型 → 分步流程 → 需要理解的要点 → 相邻文章的链接。形式化机制与参数在 +[规范](../specification)中,操作在 +[操作](../../protocol/operations/prediction-markets)中。 + +## 按角色 —— 「你就是参与者」 + +| 角色 | 讲了什么 | +|------|-------| +| [下注者](./bettor) | 我下注。浮动赔率、按份额成比例的赔付、裁定时自动赔付。 | +| [市场创建者](./market-creator) | 我开出一个问题。初始流动性、费用、预言机与争议模式的选择。 | +| [预言机](./oracle) | 我宣布结果。保险与风险下限、判错的惩罚、`reliability_score` 声誉。 | +| [主动 LP](./active-lp) | 我为市场曲线提供深度。费用收入、本金受保障、价格中性的提取。 | +| [被动 LP(懒惰池)](./passive-lp) | 我被动地存入 VIZ。来自杠杆与费用的收入、按权益计份额、FIFO 提取。 | +| [杠杆交易者](./leverage-trader) | 我用池的贷款对价格下注。固定加价 + 资金费率、清算、下注关闭时强制平仓。 | + +## 按功能 —— 「机制是如何搭建的」 + +| 功能 | 讲了什么 | +|------|-------| +| [为何是池而非赔率](./why-pool-not-odds) | 沿曲线浮动的价格(CPMM/LMSR)取代固定赔率。 | +| [预言机与裁定](./resolution) | 裁定时间线:截止期限、提前裁定、错过裁定、no-contest。 | +| [争议](./disputes) | 如何挑战一个结果:宽限窗口、保证金、投票模式、奖励/惩罚。 | +| [多元市场](./multi-outcome) | 一个市场容纳多个结果,LMSR 对比二元 CPMM。 | +| [撤注](./cancel-bet) | 按曲线定价的退出、以下注额为上限、`forfeit_pool` 的差别。 | +| [懒惰池详解](./lazy-pool) | 按权益价格计份额、收入来源、FIFO 提取、free ≥ 0 不变量。 | +| [提前退出与递延索取权](./early-exit) | 提前退出的利润是一份递延索取权,在结算时从奖池支付。 | +| [隐藏下注(提交-揭示)](./commit-reveal) | 揭示前的隐私、揭示窗口、不揭示的没收。 | +| [事件与元数据](./events-metadata) | 市场如何组装成一张比赛卡:event / event_title / child、母线与子线、节点索引了什么。 | +| [投注单(coupon)](./coupon) | 一笔交易中的多个下注:写入时的原子性、为何这不是串关、什么会让提交失败。 | + +## 接下来去哪里 + +- [规范](../specification) —— 形式化规则、参数、对象模型。 +- [白皮书](../whitepaper) —— 论点:为何流动性无风险、两种市场类型、飞轮。 +- [`pm_*` 操作](../../protocol/operations/prediction-markets) —— 已签名共识操作。 +- [工作流](../workflows) —— 一个市场贯穿所有角色。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/lazy-pool.md b/@l10n/zh-CN/docs/prediction-markets/guides/lazy-pool.md new file mode 100644 index 0000000000..1d15d99f11 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/lazy-pool.md @@ -0,0 +1,58 @@ +--- +title: "懒惰池细说 —— 份额、权益价格、FIFO 提取" +description: "懒惰池机制:份额按权益价格(free+allocated−pending_withdrawals)铸造,收益来自杠杆加价 + 资金费率 + 费用,提取走带 free_balance ≥ 0 不变量的 FIFO 队列,紧急提取的罚则仅作用于奖励。" +--- + +# 懒惰池细说:份额、权益价格、提取 + +这是对懒惰池机制的深入拆解。如果你需要「为什么、这个产品适不适合我」的总览 —— 请从[被动 LP](./passive-lp) 角色开始;这里我们讲清份额、收益与提取究竟是怎么算的。 + +## 按权益价格计的份额 + +当你存入(`pm_lazy_deposit`)时,池给你铸造的**份额**不是按 1:1 的面值,而是按当前的**权益价格**: + +> 权益 = free_balance + allocated − pending_withdrawals + +- **free_balance** —— 池的自由 VIZ,当下未被投放。 +- **allocated** —— 已投入运作的资金(杠杆贷款、深度)。 +- **pending_withdrawals** —— 已经承诺但尚未支付的提取(FIFO 队列)。 + +份额价格 = 权益 / 份额总数。为什么这样做:若只按 `free_balance` 计算,那么在资金已被投放时到来的新存款人会在奖励中拿到虚高的权重(在运作中的资金在 free 里看不见)。权益价格在老 LP 与新 LP 之间公平地分摊收益。若权益 ≤ 0(一种边缘情形),则回退为 1:1。 + +## 收益从哪里来 + +- **杠杆加价。** 杠杆贷款由池垫付;固定加价作为收益回流。 +- **资金费率。** 杠杆持仓开着的期间,从它身上按池的方向累积资金费。 +- **费用。** 系统通用费用的一份。 + +收益体现为你的份额价值上升 —— 没有什么要手动领取或复投。 + +## 提取:FIFO 队列与 free ≥ 0 不变量 + +提取(`pm_lazy_withdraw`,按份额部分提取或全部提取)会立即销毁你的份额,但支付取决于自由余额: + +- **free_balance 足够** → 支付即时到账,一如从前。 +- **不足**(资金在未平仓的杠杆里) → 一条 `pm_lazy_withdraw_request` 被登记进 **FIFO 队列**,池的 `pending_withdrawals` 字段随之增长。 + +请求在每一次带来自由余额回流的事件上**按到达顺序**兑付:杠杆平仓/清算、转换、新的存入。硬性不变量是 **free_balance ≥ 0**:池在物理上无法支付超过自由的部分。这是早期设计的一个教训:当时紧急提取会把余额拖成负数(池发出了尚未回流的资金)。 + +读取队列:`get_lazy_withdraw_requests(account)`;池的状态 —— `get_lazy_pool`;你的持仓 —— `get_lazy_deposit`。 + +## 紧急提取与罚则 + +紧急提取**只从已累积的奖励中**收取**罚则**;本金不被削减。也就是说,你投进去的总能拿回来(也许要经过队列),而着急的代价是收益的一部分,不是资金。 + +## 你需要理解什么 + +- **份额按权益定价,而非面值。** 这是对收益的公平分摊;在资金已被投放时尤为关键。 +- **提取可能在队列里等待。** 这不是损失 —— 本金会随着杠杆解开而返还;但不保证即时。 +- **free_balance ≥ 0 是铁律。** 池永远不会转负;超出自由余额的支付进入 FIFO 队列。 +- **罚则打的是奖励,而不是本金。** 紧急退出在钱上的代价小于在心态上的代价。 +- **池是杠杆的对手方。** 风险的性质与直接的市场 LP 不同(见[主动 LP](./active-lp))。 + +## 相关 + +- [被动 LP](./passive-lp) —— 角色总览(这适不适合你)。 +- [杠杆交易者](./leverage-trader) —— 谁从池借款并支付加价。 +- [主动 LP](./active-lp) —— 直接的市场流动性,用于对比。 +- [规范](../specification) —— 权益公式、提取队列、不变量。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/leverage-trader.md b/@l10n/zh-CN/docs/prediction-markets/guides/leverage-trader.md new file mode 100644 index 0000000000..44676cff81 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/leverage-trader.md @@ -0,0 +1,45 @@ +--- +title: "杠杆交易者 —— 用池的贷款押价格" +description: "杠杆:用懒惰池的贷款押某个结果「价格」的走向。固定加价 + 资金费率、清扫式清算、在下注结束时强平(不等预言机)、距 betting_expiration 不足 24 小时时无法开仓。利润是裁定后兑付的递延索取权。" +--- + +# 杠杆交易者:用池的贷款押价格 + +杠杆是叠加在市场之上的一个独立工具。你押的不是「结果直到最后」,而是某个结果**价格的走向**,并且你从懒惰池借款来放大持仓。这更接近保证金交易,而不是普通下注。 + +## 两段话讲清 + +你提交抵押品并开立杠杆持仓(`pm_leverage_open`):系统从**懒惰池**借出缺口部分,并把放大后的规模下注到所选结果上。当结果价格朝你的方向走,未实现利润增长;朝反方向走,持仓便逼近**清算**。为这笔贷款你要向池支付**固定加价(约 10%)**以及随时间累积的**资金费率**;那就是被动 LP 的收入。 + +杠杆**不等预言机**:持仓在下注关闭(`betting_expiration`)那一刻按当时的价格被强平 —— 杠杆按市场价格结算,而不是按宣布的结果。若价格更早触及阈值,持仓会被「清扫式(sweep)」清算。杠杆利润记为**递延索取权**,在市场裁定之后(从一个有界的输家池中)兑付,而不是即时到账。 + +## 逐步发生了什么 + +**开仓。** `pm_leverage_open`:你提交抵押品并设定杠杆倍数。池发放贷款(`pool.free_balance -= loan`),总规模被下注到该结果上。只有在距 `betting_expiration` **至少还有 24 小时**时才能开仓 —— 否则持仓没有存活的空间,开仓会被拒绝。 + +**持仓期间。** 结果价格随下注漂移。朝你的方向 —— 利润增长;朝反方向 —— 清算逼近。资金费率按池的方向累积。只要你持有持仓,就要为杠杆付费。 + +**主动平仓。** `pm_leverage_close` —— 你按当前价格退出:贷款返还给池,你拿走自己那一份(`cancel_value`)。价差/下限的余额(`curve_residual`)被路由进市场的 `forfeit_pool`(结算时归赢家)—— 钱不会「冻住」。 + +**清算。** 若价格触及阈值,持仓被自动平掉(sweep)。抵押品把贷款还给池;剩下的按结算规则处理。 + +**下注结束时的强平。** 若你没有自己平仓,在 `betting_expiration` 时持仓会按那一刻的价格被强平。杠杆**不依赖预言机的裁定**:它关乎价格,而不是「谁最终说对了」。 + +**利润的兑付。** 杠杆利润不是即时现金:它是一份**递延索取权**,在市场裁定之后从一个有界的输家质押/没收池中兑付(赔付带上限)。这样系统就不会凭空增发代币。 + +## 杠杆交易者需要理解什么 + +- **你押的是价格,而不是结果。** 杠杆在下注结束时按价格平仓,而不是在预言机宣布时。你可以把「价格」押对,却根本没见到结果 —— 这是两回事。 +- **贷款来自池,而且不是免费的。** 固定加价加上资金费率都归懒惰池。长期持有持仓很贵;随着时间推移,资金费对你不利。 +- **清算是真实的。** 走势对你不利会强制平掉持仓,抵押品用于偿还贷款。杠杆放大利润,也放大亏损。 +- **24 小时窗口。** 距下注结束不足一天时无法开立杠杆 —— 持仓需要空间去存在、并正确地平仓。 +- **利润在裁定之后到账。** 不要指望即时提走杠杆的盈利:它记为递延索取权,在市场结算时于可用池的范围内兑付。 +- **给懂它的人用的工具。** 杠杆比普通下注复杂;如果你想简单地押结果,见《下注者》一文。 + +## 与你相邻的角色 + +- **被动 LP(懒惰池)** —— 你向其借款的那一方;你的加价与资金费是他们的收入。 +- **下注者** —— 不借款、不会被清算地押结果;更简单的路。 +- **预言机** —— 裁定市场;它间接影响你的杠杆(利润在裁定后结算),但杠杆平仓绑定的是价格,而不是他们的裁决。 + +延伸阅读:《被动 LP(懒惰池)》(你这笔贷款的另一面)、《杠杆 —— 机制》(加价与清算公式)、《提前退出与递延索取权》(杠杆利润如何、何时到账)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/market-creator.md b/@l10n/zh-CN/docs/prediction-markets/guides/market-creator.md new file mode 100644 index 0000000000..4a76bd15f6 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/market-creator.md @@ -0,0 +1,51 @@ +--- +title: "市场创建者 —— 如何在 VIZ 上开出一个问题" +description: "如何创建一个预测市场:设定问题与结果、投入初始流动性、选择预言机与争议模式、设定费用。流动性本金受保障,并在结算时自行返还。" +--- + +# 市场创建者:如何开出一个问题 + +你来拟定人们将要下注的问题:「谁会赢」、「事件是否会在某个日期前发生」、「是/否」。在 VIZ 上任何账户都可以创建市场 —— 这是一个普通的已签名 `pm_create_market` 操作,无需权限、无需审核。我们来走一遍:你要设定什么、你要为什么负责。 + +## 两段话讲清 + +一个市场 = 一个问题 + 一组结果 + 一条用来计算价格的**流动性曲线**。你投入初始流动性(你自己的资金),设定市场费用,选择**预言机**(谁宣布结果)与**争议模式**(结果可以如何被挑战)。创建之后市场自行运转:人们下注、价格浮动、到期限时预言机宣布结果、节点自动计算赔付。 + +你的初始资金不会「烧掉」—— 直接的市场流动性是**本金受保障**的:它决定曲线深度、从费用中赚取收益,并在结算时**全额返还**给你(外加你累计的费用份额)。你不是下注的对手方,也不为结果承担本金风险 —— 这正是 VIZ 市场与可能亏成负数的「博彩公司」之间的区别。 + +## 创建时你要设定什么 + +**问题与结果。** 问题文本、元数据(标题、事件、标签、图片链接)。市场类型:**二元**(两个结果,CPMM 曲线)或**多元**(最多 64 个结果,LMSR)。当结果很多时(N 个中谁会赢)多元市场更方便;详情 —— 见关于多元市场的文章。 + +**初始流动性。** 你向曲线投入多少 VIZ。越多,市场就越「深」:下注推动价格更平缓,大玩家可以在没有剧烈跳变的情况下入场。这是你本金受保障的资本(见关于主动 LP 的文章)。 + +**市场费用。** 从下注中扣留、并作为收入付给流动性提供者(你和其他 LP)的百分比。上方受一个网络中位数参数限制。 + +**预言机。** 负责宣布结果的账户。可以是你自己,也可以是受信任的专业预言机(例如镜像 Polymarket/Kalshi 的那种)。预言机持有**保险保证金**与声誉:裁定错误或错过裁定都会受罚 —— 见关于预言机的文章。 + +**争议模式与窗口。** 结果宣布之后可以如何被挑战:由委员会还是由账户投票,以及在多长的宽限期内。这保护下注者免受错误裁定的影响。 + +**期限。** `betting_expiration` —— 何时停止接受下注;`result_expiration` —— 预言机必须宣布结果的截止期限。`allow_early_resolution` 选项允许预言机在结果已经确定时提前关闭市场。 + +## 创建之后会发生什么 + +- **市场开放。** 人们下注,费用作为 LP 收益累加给你。你可以追加流动性,或提取其中一部分(价格中性,不损失本金)。 +- **下注关闭。** 到 `betting_expiration` 时停止接受下注。 +- **裁定。** 预言机宣布结果。节点分配池:判对的人自动获得赔付,而你的 LP 本金 + 费用份额在结算时**无条件**返还。 +- **如果没有结果。** 预言机宣告 **no-contest**(事件被取消、来源消失)→ 下注退还给下注者,你的流动性回到你手中,且预言机不受罚。但如果预言机**一直沉默**到截止期限(`result_expiration` + 宽限期),市场会作为**错过裁定(missed-resolution)**死去:下注仍然退还,但预言机受罚(见「[预言机与裁定](./resolution)」)。 + +## 创建者需要理解什么 + +- **你不是庄家。** 直接的市场流动性不会用你自己的钱去承担赢利 —— 奖池由失败的下注构成。你的本金受保障。 +- **预言机是关键选择。** 市场能否被诚实裁定取决于它。糟糕的预言机 = 争议与惩罚。要么选一个久经检验的,要么自己做预言机并把保险保持在风险下限之上,否则你的市场会从目录中被隐藏。 +- **费用是一种平衡。** 费用越高 = 你的收入越多,但市场对玩家越贵、成交量越少。费用越低则对下注者越有吸引力。 +- **深度决定一切。** 流动性稀薄 = 每笔下注都让价格剧烈跳变,会赶走大玩家。初始资金决定市场的质量。 +- **元数据很重要。** 没有链上标题/标签的市场在客户端里更难被找到,自动化也更难裁定它。 + +## 你身边的角色 + +- **下注者** —— 对你的结果下注的人。 +- **预言机** —— 宣布结果;由你在创建时选择。 +- **主动 LP** —— 可以在你的初始资金之上为你的市场追加深度。 + +延伸阅读:「预言机与裁定」(该选谁、他们负什么责)、「主动 LP」(流动性与本金返还的机制)、「争议」(争议模式)、「多元市场」(结果多于两个时)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/multi-outcome.md b/@l10n/zh-CN/docs/prediction-markets/guides/multi-outcome.md new file mode 100644 index 0000000000..fb55d2b520 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/multi-outcome.md @@ -0,0 +1,39 @@ +--- +title: "多元市场 —— 一个问题,多个选项" +description: "多元市场:一个市场容纳若干结果(N 个之中哪一个获胜),由 LMSR 定价,而非二元的 CPMM。结果数量的上限是中位数参数 pm_max_outcomes;超限的市场会被降为二元形式。下注、份额与赔付的运作和任何彩池一样。" +--- + +# 多元市场:一个问题,多个选项 + +并非每个问题都归结为「是/否」。「八支队伍的锦标赛谁会赢?」、「哪个党派得票最多?」—— 这里有很多结果。多元市场把它们放在**一个**市场里,而不是一堆各自独立的二元市场。 + +## 两段话讲清 + +二元市场(两个结果)沿 **CPMM** 曲线定价。当结果更多时,市场使用 **LMSR**(对数市场评分规则):一条曲线同时服务所有结果,其中每个结果的价格取决于相对于其他结果、在它上面押了多少。各结果「概率」之和保持一致,而你随时能看到每个选项的相对价格。 + +对作为参与者的你来说,逻辑与任何彩池相同:你对某个结果下注,按当前价格获得份额(权重),裁定时奖池按份额比例分配给判断正确的人。区别在引擎盖之下(定价公式),以及在上限上:结果的数量受网络中位数参数 `pm_max_outcomes` 约束;如果结果比上限还多,市场会被降为二元形式(例如「热门 vs 其余」)。 + +## 它与一捆二元市场有何不同 + +**一个市场,而不是 N 个。** 不是十个各自独立的「X 队获胜:是/否」市场 —— 而是一个带十个结果的市场。流动性不会被摊薄到十个彩池里,而各选项之间的价格是一致的。 + +**LMSR 而不是 CPMM。** 二元的 CPMM 持有两侧;LMSR 通过一个对数成本函数把这一点推广到多个结果。LMSR 的「深度」参数(`lmsr_b`)由所提供的流动性与结果数量推导而来 —— 它决定推动价格有多贵。流动性越多,市场越深。 + +**价格是一致的。** 在 LMSR 中各结果的价格是联动的:一个变贵,其余就相对变便宜。这比一组彼此独立的二元市场更接近一个「概率分布」。 + +**与事件的关系。** 大型事件(一场比赛、一届锦标赛)常常从外部来源镜像为一组共用同一个 `event` 键的市场 —— 于是多元市场与相关的二元衍生盘会在事件页面上被归到一起(见 Forecaster 客户端)。 + +## 你需要理解的地方 + +- **下注的逻辑没有变。** 结果 → 按当前价格得到份额 → 裁定时分得奖池的一份。多元的差别在定价,而不在实质。 +- **结果上限是一个网络参数。** `pm_max_outcomes` 由中位数投票决定;结果比上限更多的市场会以二元形式创建,而不是被无声拒绝。 +- **结果越多,深度越要紧。** 多元市场上流动性稀薄时价格移动得更剧烈 —— LMSR 的深度被摊到所有选项上。 +- **早期进入定价过低的选项是划得来的。** 与二元彩池一样:价格便宜意味着份额更多。 +- **赔付随份额而定。** 没有预先固定的赔率;结果出自最终的分配。 + +## 相关 + +- [为何是彩池而非赔率](./why-pool-not-odds) —— 价格在彩池里究竟是如何形成的。 +- [下注者](./bettor) —— 一笔下注的生命周期(对多元市场也一样)。 +- [市场创建者](./market-creator) —— 创建时如何选择市场类型。 +- [规范](../specification) —— LMSR 公式、`lmsr_b`、结果上限。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/oracle.md b/@l10n/zh-CN/docs/prediction-markets/guides/oracle.md new file mode 100644 index 0000000000..f2de0a8539 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/oracle.md @@ -0,0 +1,47 @@ +--- +title: "预言机 —— 如何宣布结果并为之负责" +description: "预言机宣布市场结果(pm_resolve_market),持有保险保证金与声誉(reliability_score)。裁定错误/错过裁定会受罚;风险下限会把资金不足的预言机从目录中隐藏。" +--- + +# 预言机:如何宣布结果并为之负责 + +预言机是那个告诉链上「事件如何结束」的账户。奖池归谁取决于它。正因如此,VIZ 上的预言机不只是「按一个按钮」:它持有一笔**资金保证金**与一份**声誉**,并以两者为诚实与及时负责。 + +## 两段话讲清 + +你用 `pm_resolve_market` 操作宣布获胜结果(只能在下注关闭之后)。对判对者的赔付会自动记入。要被托付市场与资金,你需要持有**保险** —— 一笔绑定在你的预言机账户上的 VIZ 保证金。如果保险相对于你所服务的下注量跌到**风险下限**之下,节点会**把你的市场从目录中隐藏** —— 这一机制保护下注者免受资本不足的预言机之害。 + +对于错误与沉默,你会从累计的声誉与保证金中受罚:裁定错误、被争议挑战并推翻;错过截止期限(`result_expiration`),此时市场作为错过裁定(missed-resolution)死去。你的**声誉**(`reliability_score`,0..10000 bp)由准确率、争议裁决结果、响应率与及时性构成 —— 并且对每一个挑选预言机的人都是可见的。 + +## 你分步要做什么 + +**做好准备。** 为预言机账户注入保险(`pm_oracle_update`)—— 把它保持在风险下限之上并留有余量,否则你的市场会被隐藏。下限规则:一个市场可见的条件是保险 ≥ 阈值,且 ≥ 你所管理的下注总额的某个倍数。 + +**等待下注关闭。** 裁定只能在 `betting_expiration` 之后进行。不能更早(在下注还在进来的时候宣布结果毫无意义)。如果市场创建时带有 `allow_early_resolution` 且结果已经确凿,你可以提前关闭它 —— 此时争议窗口会收缩到裁定的那一刻。 + +**宣布结果。** 用 `pm_resolve_market` 提交获胜结果。节点分配池:判对的人自动获得赔付(虚拟 `pm_payout`),失败的结果被清零。对于这次裁定,你会获得一笔**预言机费用**(受网络中位数参数限制,最多 `pm_max_oracle_fee_percent`)。 + +**如果没有结果。** 事件被取消、来源消失、平局无胜者 → 你不做裁定,而是宣告 **no-contest**(`pm_no_contest`):下注被退还,无人赢也无人输。这是一次诚实的退出,不是惩罚 —— 但必须及时完成,在截止期限之前。 + +**度过争议窗口。** 宣布之后,下注者可以在宽限期内挑战结果(`pm_dispute_create`)。如果争议认定你是对的,你的声誉得到确认;如果你的裁定被认定错误,你会受罚。详情 —— 见关于争议的文章。 + +## 你会因什么受罚 + +- **裁定错误。** 你宣布了错误的结果,且争议予以确认 → 从保证金/声誉中受罚,赔付被重算。 +- **错过截止期限。** 你没有在 `result_expiration` 之前宣布结果 → 市场作为错过裁定死去,你被罚没。不要沉默:如果来源没有给出结果,就宣告 no-contest。 +- **系统性迟缓。** 迟到(但完成了)的裁定会拉低 `reliability_score` 中的及时性因子。迟到不再等同于按时。 + +## 预言机需要理解什么 + +- **保险就是用数字表达的信任。** 把它保持在下限之上并留有余量;它会因累积的罚没而下沉,并随你所服务的成交量而增长。资金不足的预言机会从目录中消失 —— 市场看起来像是「不见了」。 +- **声誉是公开且综合的。** `reliability_score` = 准确率 + 争议裁决结果 + 响应率 + 及时性 − 惩罚(衰减的故障印记 penalty-stamps) − 封禁,并在裁定数量较少时向平均值收缩。界面中以百分比显示(bp/100)。 +- **裁定的效果不可逆,但可被争议。** 赔付会立即发出,但争议窗口可以取消它们并惩罚你。如果你判错了,自己发起更正比等着被罚没更诚实。 +- **及时性 = 金钱。** 及时的 no-contest 胜过沉默到截止期限。如果你服务很多市场,就把裁定自动化。 + +## 你身边的角色 + +- **市场创建者** —— 选择你作为预言机,并设定争议窗口。 +- **下注者** —— 把结果托付给你,也可以挑战它。 +- **争议者** —— 争议的参与者,检验你的裁决。 + +延伸阅读:「争议」(结果如何被挑战、这对预言机意味着什么)、「市场创建者」(谁指派你、如何指派)、「预言机与裁定」(期限、错过裁定、no-contest 详解)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/passive-lp.md b/@l10n/zh-CN/docs/prediction-markets/guides/passive-lp.md new file mode 100644 index 0000000000..6b21b3ceab --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/passive-lp.md @@ -0,0 +1,40 @@ +--- +title: "被动 LP —— 懒惰池" +description: "懒惰池是一个被动产品:你存入 VIZ,从杠杆加价与费用中赚取收益。份额按权益价格铸造,提取走 FIFO 队列(free_balance ≥ 0 不变量),罚则仅作用于奖励。池是杠杆交易者的对手方。" +--- + +# 被动 LP:懒惰池 + +不想挑选某个具体市场、盯着它的深度 —— 那就把 VIZ 放进**懒惰池**。它是一个被动产品:你的资金自动为系统(首先是杠杆)提供燃料并赚取收益,而你什么都不用盯。 + +## 两段话讲清 + +你存入 VIZ(`pm_lazy_deposit`)并按池当前的**权益价格**获得池的**份额**(不是按面值:份额价格由 `free + allocated − pending_withdrawals` 计算,这样在资金已被投放时新存入的资金不会在奖励中拿到虚高的权重)。池充当**杠杆交易者的对手方**:杠杆贷款由懒惰池垫付,而加价外加资金费率则作为收益回流到池中。除此之外还有系统通用费用的一份。 + +你在池中的本金会回来,但如果当下自由余额不足(资金投放在未平仓的杠杆持仓里),这笔支付可能进入 **FIFO 队列**。池的不变量是 `free_balance ≥ 0`:池永远不会支付超过实际自由的部分;提取请求会被登记,并随着资金回流逐步兑付。提前提取的罚则**只从奖励中**扣取 —— 本金不会被削减。 + +## 逐步发生了什么 + +**存入。** `pm_lazy_deposit` —— VIZ 进入池,你按权益价格获得份额。此后由池自己决定把资金投向何处(杠杆、深度);这件事你不手动操控。 + +**收益累积。** 杠杆交易者支付加价与资金费 —— 这些进入池的收益。你的份额随之增值。收入是被动的,没有什么要点击的。 + +**提取 —— 计划提取或紧急提取。** `pm_lazy_withdraw`(按份额部分提取,或全部提取)。若池有足够的自由余额,支付即时到账。若资金已被投放,一条请求(`pm_lazy_withdraw_request`)进入 **FIFO 队列**,并随着资金从杠杆/深度回流而兑付。紧急提取要付罚则 —— 但**只从已累积的奖励中**扣;你的本金不减少。 + +**队列及其顺序。** 请求在每一次带来自由余额回流的事件(杠杆平仓、转换、新的存入)上按到达顺序兑付。这保护池不会转为负数 —— 这是早期设计的一个教训:当时紧急提取会把 `free_balance` 拖到零以下。 + +## 被动 LP 需要理解什么 + +- **池是杠杆的对手方。** 与直接的市场 LP(曲线深度、对结果本金保障)不同,懒惰池承担杠杆持仓的风险:它的资金被交易者借走。收益更高,但风险的性质不同。 +- **份额价格基于权益,而非面值。** 你按池的真实价值获得份额,而不是 1:1。这在老存款人与新存款人之间公平地分配收益。 +- **提取可能需要等待。** 若全部自由余额都已投放,你的请求进入队列。这不是损失 —— 本金会随着持仓解开而返还;但不保证即时。 +- **罚则只打奖励。** 提前/紧急提取削减的是收益,不是本金。`free_balance ≥ 0` 是硬性不变量。 +- **被动既是优点也是缺点。** 你不挑市场、不盯深度,但你同样不控制资金流向何处。 + +## 与你相邻的角色 + +- **杠杆交易者** —— 从你的池借款去押价格;他们的加价就是你的收入。 +- **主动 LP** —— 精神上相反的产品:在具体市场里手动做深度,对结果本金保障。 +- **市场创建者** —— 搭建杠杆与深度赖以运作的那些市场。 + +延伸阅读:《杠杆交易者》(谁从池借款、怎么借)、《主动 LP》(与直接流动性有何不同)、《懒惰池细说》(权益价格、FIFO 提取、收益逐步拆解)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/resolution.md b/@l10n/zh-CN/docs/prediction-markets/guides/resolution.md new file mode 100644 index 0000000000..147188e405 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/resolution.md @@ -0,0 +1,49 @@ +--- +title: "预言机与裁定 —— 结果如何被宣布" +description: "市场裁定机制:只有在下注结束后才能裁定、result_expiration 截止时点、提前裁定(allow_early_resolution)、宽限期之后的错过裁定作废、无结果时的 no-contest。赔付是自动的。" +--- + +# 预言机与裁定:结果如何被宣布 + +本文讲的是裁定的**机制** —— 时间线,以及对所有参与者通用的规则。至于预言机本身的职责与风险,另有一篇文章 [预言机](./oracle);这里我们看的是一个市场如何从下注结束走到赔付。 + +## 两段话讲清 + +一个市场只能在**下注结束之后**(`betting_expiration`)被裁定 —— 只要下注还在进来,宣布结果就没有意义。市场有一个 `result_expiration` 截止时点,预言机必须在此之前宣布结果(`pm_resolve_market`)。裁定时,对判断正确者的赔付会**自动**入账(虚拟操作 `pm_payout`),落败结果被清零,其资金进入赢家的奖池。 + +如果没有结果 —— 事件被取消、来源消失、平局且无赢家 —— 市场作为 **no-contest**(`pm_no_contest`)关闭:下注被退款,无人赢也无人输。而如果预言机在截止时点和宽限期之后仍然沉默,市场则作为**错过裁定**(missed-resolution)死去(预言机受罚)。此外还有一条提前的路径:带 `allow_early_resolution` 的市场若结果已经确定无疑,可以更早关闭。 + +## 市场时间线 + +**1. 开放。** 下注在进来,价格沿曲线浮动。禁止裁定。 + +**2. 下注结束(`betting_expiration`)。** 不再接受下注。开启预言机可以(且必须)宣布结果的窗口。 + +**3. 裁定(`pm_resolve_market`)。** 预言机宣布获胜结果。节点拆分彩池:赔付是自动的,没有什么需要手动领取。因这次裁定,预言机取得自己的费用(受一个中位数参数上限约束)。 + +**4. 争议窗口。** 宣布之后是宽限期(`pm_dispute_grace_sec`),期间结果可被质疑(见[争议](./disputes))。在争议终结之前,赔付并非最终。 + +**5. 结算。** 按已确认的结果结算:LP 流动性被返还(本金受保障),奖池被分配。 + +## 特殊路径 + +**提前裁定(`allow_early_resolution`)。** 若市场创建时带了这个开关,且结果已经确定无疑,预言机可提前关闭它。此时 `result_expiration` 会移到裁定发生的那一刻(争议窗口收缩为「现在 + 宽限期」),但提出争议者仍然保有自宣布起完整的 `pm_dispute_grace_sec`。而迟到的裁定(在 `result_expiration` 之后)恰恰相反,不会延长窗口。 + +**No-contest(`pm_no_contest`)。** 没有结果 —— 市场被取消,下注被退款。这不是对参与者的惩罚:不会因为某个来源沉默了就让他们的钱「烧掉」。如果注定不会有结果,预言机必须及时宣告 no-contest。 + +**错过裁定(到截止时点作废)。** 预言机在 `result_expiration` + 宽限期之前既没有宣布结果,也没有宣告 no-contest → 定时任务作废该市场,并罚没预言机。重要的是:作废只在 `result_expiration + pm_dispute_grace_sec` **之后**触发(与结算清扫相同的截止线)—— 这样预言机才有一个真实的裁定窗口,而不是与截止时点赛跑(这一点由可达性修复解决;否则一个没有提前开关的固定截止市场根本无法被裁定)。 + +## 需要理解的地方 + +- **只有在下注结束后才裁定。** 不能更早;提前的路径是市场的一个单独开关。 +- **赔付是自动的。** 没有「领取」,没有按钮:奖金与退款在裁定/取消时到账。 +- **沉默要受罚。** 没有结果 → 及时宣告 no-contest。单纯「什么都不做」= 错过裁定,并罚没预言机。 +- **在宽限期结束之前,结果不是最终的。** 在争议窗口内赔付可以被重算。 +- **杠杆不等裁定。** 杠杆持仓按 `betting_expiration` 时点的价格关闭,与预言机的裁决无关 —— 见[杠杆交易者](./leverage-trader)。 + +## 角色与链接 + +- [预言机](./oracle) —— 谁来宣布结果,以及用什么来担责。 +- [争议](./disputes) —— 如何质疑已宣布的结果。 +- [下注者](./bettor) —— 从一笔下注的角度看裁定是什么样。 +- [规范](../specification) —— 形式化的截止时点与截止线。 diff --git a/@l10n/zh-CN/docs/prediction-markets/guides/why-pool-not-odds.md b/@l10n/zh-CN/docs/prediction-markets/guides/why-pool-not-odds.md new file mode 100644 index 0000000000..28c281b3fc --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/guides/why-pool-not-odds.md @@ -0,0 +1,39 @@ +--- +title: "为何是池而非赔率" +description: "VIZ 上没有固定赔率:结果的价格由池曲线决定(二元市场用 CPMM,多元市场用 LMSR),赔付是奖池中按所买份额成比例的一份。赔率在下注关闭前一直浮动。" +--- + +# 为何是池而非「赔率」 + +「这个结果的赔率是多少?」是最常见的问题。在 VIZ 上诚实的回答是:**没有固定赔率**,而这是一条原则,不是疏漏。我们来走一遍:池与博彩公司有什么不同,你的赢利又是如何从其中产生的。 + +## 两段话讲清 + +在博彩公司那里,赔率是由**商家**报出的 —— 它是对手方,它承担风险,它靠抽水赚钱。总得有人来当「庄家」。在区块链上没有「庄家」:否则那又是一个中介,而这恰恰是生态要去掉的东西。所以 VIZ 市场是一个**共享池**,结果的价格由我们自己、由我们自己的下注决定。 + +价格既不是凭空捏造的,也不会被锁定:它由**池曲线**决定(二元市场用 CPMM,多元市场用 LMSR)。每一笔下注都会推动价格:某个结果上押得越多,它就越贵,下一枚代币能买到的「份额」(权重)就越少。裁定时,输家的奖池**按份额比例**分给判对的人。事先不会承诺什么「×2.5」—— 倍数产生于所有人最终的持仓分布。 + +## 它如何运作 + +**价格就是曲线此刻的状态。** 当你下注时,节点沿曲线计算你的金额在当前价格下应得多少份额。对不受欢迎的结果早下注,意味着你入场便宜、拿到很多份额。等人群涌入,价格就不同了。 + +**简单的「彩池」直觉。** 大致来说:「是」上总共 100 Ƶ,「否」上 300 Ƶ,而「是」发生了。赢家取回自己的钱,并按份额分掉失败的那 300 Ƶ。一笔 10 Ƶ 的下注(「是」池的十分之一)→ 出来时约 40 Ƶ。确切数字来自曲线(平滑的价格,而非纯彩池的阶梯),但方向是一样的:获胜一侧的人越少,分到的份就越肥。 + +**赔率在下注关闭前一直浮动。** 由于赔付是由活的下注构成的,它会在市场开放期间变化。一个大玩家进入你这一侧 —— 你在池中的份额缩小,结果会**低于**你下注时看起来的样子。这不是界面在骗你,这是池的本性。正因如此客户端显示的是一个估算,而不是「固定赔率」。 + +**深度让一切更平滑。** 流动性(来自 LP 的直接流动性,加上创建者的初始资金)决定一笔下注推动价格的剧烈程度。深市场意味着平滑的价格,稀薄的市场意味着跳变。流动性在市场存活期间工作,并在结算时返还(本金受保障)。 + +## 你需要理解什么 + +- **没有庄家就没有固定赔率。** 价格由所有参与者共同决定;你看到的是当前估算,不是承诺。 +- **赔付是奖池中的一份,而非「下注额 × 赔率」。** 结果取决于下注在各结果上的最终分布,而不是你入场时看到的那个数字。 +- **在定价偏低的结果上早入场回报更高。** 价格还低的时候,每枚代币能换到更多份额。 +- **不要下注超过你准备承受损失的金额。** 浮动赔率甚至在下注关闭之前就可能对你不利。 +- **参数活在链上,而不在某个网站上。** 费用、惩罚、限额 —— 由代表中位数投票决定,而不是某个商家的意志。 + +## 接下来去哪里 + +- [下注者](./bettor) —— 如何分步下注。 +- [主动 LP](./active-lp) —— 谁决定曲线深度、为何本金受保障。 +- [多元市场](./multi-outcome) —— 结果多于两个时(LMSR)。 +- [规范](../specification) —— 曲线与结算的形式化公式。 diff --git a/@l10n/zh-CN/docs/prediction-markets/index.md b/@l10n/zh-CN/docs/prediction-markets/index.md new file mode 100644 index 0000000000..2261354e16 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/index.md @@ -0,0 +1,49 @@ +--- +title: 预测市场(Onix)—— 总览与地图 +description: VIZ 链上预测市场技术栈——Onix 协议与 Forecaster 客户端——从白皮书到规范、对象、操作、工作流与概念分析的完整文档树。 +--- + +# VIZ 上的预测市场 + +VIZ Ledger 将预测市场作为**一等公民共识操作**(`pm_*`)运行,自 HF14 上线。全文会出现两个名称: + +- **Onix** —— **协议**:链上市场引擎(CPMM 二元 + LMSR 多元、同注分彩零和结算、保证金预言机、懒惰池、可选 + 杠杆、批量 / 提交-揭示下注)。 +- **Forecaster** —— 对接该协议的 VIZ Ledger **瘦客户端**。它是一个无头、与平台无关的前端,让**全球用户参与链上 + 预测市场**——创建市场、下注、提供流动性、做预言机、发起争议——通过直接对公共 VIZ 节点签署 `pm_*` 操作。协议 + 中立;Forecaster(以及任何按其方式构建的司法辖区客户端)是接入层。 + +## 文档地图 + +```mermaid +flowchart TD + ROOT["预测市场(Onix)"] + ROOT --> OV["总览 — 一页式定位(整体一览)"] + ROOT --> WP["白皮书 — 论点:为何 LP 无风险、两种市场类型、飞轮"] + ROOT --> SP["规范 — 形式化机制 + §17 链上对象模型"] + ROOT --> OPS["操作 — 已签名的 pm_* 共识操作"] + ROOT --> VOPS["虚拟操作 — 结算时/到期时发出的确定性 vop"] + ROOT --> API["插件 API — prediction_market_api 只读方法"] + ROOT --> WF["工作流与图 — 一个典型市场贯穿每个角色"] + ROOT --> CA["概念分析 — 90 个 PM 理论概念对照 VIZ 实现"] +``` + +## 从这里开始 + +| 页面 | 是什么 | +|------|-----------| +| [总览](./onix) | 一页式定位:AMM 定价的同注分彩,具备结构性无风险流动性。 | +| [白皮书](./whitepaper) | 产业论点——LP 保障、Onix Binary(CPMM)+ Onix Multi(LMSR)、预言机、懒惰池、杠杆、治理。 | +| [规范](./specification) | 形式化规范:参数、状态机、定价、结算、争议、懒惰池、杠杆,以及 **[链上对象模型](./specification)**(每个 `pm_*_object` 及其查找索引)。 | +| [操作](../protocol/operations/prediction-markets) | 21 个已签名共识操作(`pm_create_market`、`pm_place_bet`、…)。 | +| [虚拟操作](../protocol/virtual-operations) | 确定性 vop(`pm_payout`、`pm_market_accepted`、`pm_leverage_resolve`、`pm_batch_settle`、…)。 | +| [插件 API](../plugins/prediction-market-api) | `prediction_market_api` —— 对市场、下注、预言机、争议、懒惰池及中位数投票参数的只读访问。 | +| [工作流与交互图](./workflows) | 一个典型二元市场贯穿所有参与者,附正常与争议裁定的零和主账本。 | +| [概念分析(Onix 对照 90)](./concepts-analysis) | 链上实现如何映射到预测市场理论图谱——哪些已解决、固有、不需要或在路线图。 | + +## 治理 + +所有经济参数均由代表**中位数投票**,位于 `chain_properties_pm` 结构体——见 +[链参数 → 预测市场参数](../governance/chain-properties#pm-parameters)。调整费用、罚则、懒惰池、杠杆或 +批量/提交-揭示时序无需硬分叉;三个实时终止开关(`pm_commit_reveal_enabled`、`pm_lazy_pool_enabled`、 +`pm_leverage_enabled`)让验证者中位数无需分叉即可停用整个子系统。 diff --git a/@l10n/zh-CN/docs/prediction-markets/onix.md b/@l10n/zh-CN/docs/prediction-markets/onix.md new file mode 100644 index 0000000000..0035492b83 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/onix.md @@ -0,0 +1,71 @@ +--- +title: Onix —— AMM 定价的同注分彩预测市场 +description: Onix 在同注分彩(parimutuel)结算之上叠加连续的 AMM 价格发现——价格像 AMM 一样波动,而流动性具备彩池的风险特征:做市商永远不会破产。 +--- + +# Onix —— AMM 定价的同注分彩预测市场 + +> 在 **同注分彩(parimutuel)结算** 之上叠加连续的 **AMM 价格发现**——价格像 AMM 一样波动,而流动性具备彩池的风险特征:**做市商永远不会破产。** + +::: info Onix 与 Forecaster +**Onix** 是链上协议。**Forecaster** 是 VIZ Ledger 上对接它的瘦客户端——无头、与平台无关的接入层,让全球用户 +通过直接对公共 VIZ 节点签署 `pm_*` 操作来参与链上预测市场。完整文档树见[板块总览与地图](./)。 +::: + +## 一个核心思想 + +Onix 将**价格与赔付解耦**: + +- **价格(发现)**——CPMM 曲线(二元)或 LMSR-softmax(多元)在每次下注时更新实时概率,并为每笔下注分配一个**权重**(其份额凭证)。 +- **赔付(结算)**——赢家**只**从输家被没收的本金中获得赔付,按权重分配:纯**同注分彩**,严格零和(协议从不增发代币)。 + +Onix 所有的独特之处都源于这一解耦。 + +## 为什么重要 —— 三点 + +::: tip 1 · 无法被抽干的流动性 +由于赢家从输家而非从 LP 本金中获得赔付,流动性提供者**无法破产**——没有无常损失、没有库存风险、不会被狙击致死。这由*构造保证*(CPMM 用 AM–GM,LMSR 用守恒),而非靠保险。 +::: + +::: tip 2 · 无 IL 的被动收益 —— 懒惰池 +一次存入即自动作为隐形流动性铺展到多个市场,并为可选杠杆提供资金,采用 MasterChef 式的奖励记账。**无需**挑选市场、**不**承担无常损失即可赚取预测市场的流动性收益。 +::: + +::: tip 3 · 链原生、零和 +市场是一等公民的共识操作(`pm_*`),而非智能合约:抗审查、可组合、约 3 秒出块、无需预言机桥。协议从不增发代币——只做再分配。 +::: + +## 一笔下注如何运作 + +1. **你下注** `X` 于某结果。`X` 进入曲线;曲线返回你的**权重**——越早下注(在价格移动之前),权重越多。 +2. **赔率板更新。** 某一方的实时系数 = `1 + 对方池 × (1 − 佣金) / 本方池`,佣金(预言机 + 创建者 + LP)已内含其中。 +3. **结算时**,输家本金(扣除佣金)按权重在赢家间分配。你的赔付 = 退回本金 **+** 你在输家池中的份额。LP 本金原封不动地返还。 + +## 对比 + +| | CLOB / AMM(Polymarket、Kalshi) | 纯同注分彩(彩池) | **Onix** | +|---|---|---|---| +| 实时价格 | 是 | 否(仅池比) | **是(CPMM / LMSR)** | +| 下注时锁定赔率 | 是 | 否 | 否(诚实的同注分彩) | +| LP / 做市商可能破产 | **是**(IL、狙击、跳空风险) | 不适用 | **否(结构性)** | +| 带收益的流动性层 | 脆弱 | 无 | **懒惰池,无 IL** | +| 运行于 | 合约 / 后端 | 后端 | **共识(`pm_*`)** | +| 代币增发 | 有时 | 否 | **否(零和)** | + +## 诚实的取舍 + +::: warning 赔率是同注分彩——会漂移到收盘 +Onix **不**在下注时锁定你的系数。赔率板随资金流动而移动,最终系数只有在收盘时才确定——正如彩池。这不是需要打补丁的缺陷:锁定赔率的*唯一*方式是让对手方承担风险(庄家,或可能亏损的 AMM LP)。Onix 的漂移正是其 LP 保障的直接代价——风险存在于**下注者之间**,所以任何人的流动性都不会被烧毁。 +::: + +## 新颖之处 + +- **AMM 加权 + 同注分彩结算** 集成于同一引擎——连续价格发现,*不带*做市商库存风险。 +- **结构性、可证明的 LP 安全**,而非依赖保险或补贴的流动性。 +- **共担的、带收益的流动性层**(懒惰池),同时为可选杠杆提供资金——清算按下注前的储备执行,因此池始终被补足。 +- **可选的抗 MEV**(批量 / 提交-揭示下注)与**透明治理**(保证金预言机、可修改投票的公开听证争议)——全部叠加在安全基座之上,从不触碰 LP 保障。 + +## 了解更多 + +- 协议操作 —— [预测市场](../protocol/operations/prediction-markets) +- 插件 API —— [预测市场 API](../plugins/prediction-market-api) diff --git a/@l10n/zh-CN/docs/prediction-markets/parlay-spec.md b/@l10n/zh-CN/docs/prediction-markets/parlay-spec.md new file mode 100644 index 0000000000..d0896ed2b4 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/parlay-spec.md @@ -0,0 +1,230 @@ +# 过关(Parlay / 串关)与系统投注 —— 已考虑并否决 + +状态:**作为共识原语被否决**(owner 决定 2026-08-19,q#603=A)。下面完整的 +设计作为一份归档记录保留下来,说明*为什么*这个想法不适合协议,好让下一个「我们加个过关吧」的提案 +从这份论证充分的否决出发,而不是从零开始。 + +## 为什么被否决 + +平台的核心信任不变量是**流动性提供者和 Lazy Pool 在构造上受本金保障**:下注在下注者之间零和 +(赢家分输家的池子),池子/LP 只收费用和下限。存款人不必把自己的本金托付给市场创建者或预言机。 +正是这个不变量让一个 permissionless 创建市场、预言机互相竞争的*去中心化*预测市场从根本上可行。 + +真正的过关需要一个在投注时刻锁定赔率、持有方向性风险的对手方。把 Lazy Pool 变成这个对手方就 +破坏了这个不变量——存款人变成每个市场创建者质量的抵押品——而且这不是靠参数能修好的: + +1. **腿的相关性是一个结构性的逆向选择洞。** `W = S·(1−m)/Π p_i` 只对*相互独立*的腿才公平。在 + permissionless 的世界里,市场创建者可以随意构造相关的腿(「X 赢得比赛」+「X 赢得第二局」—— + 同一个现实事实被两个不同的预言机包装)。`Π p_i` 系统性地低估这类组合,给攻击者一个对池子 + 持续的正 EV。市场之间的相关性是现实世界的语义——原则上在链上无法检测。中心化博彩公司用人类 + 交易员和 per-combo 限额解决它;协议没有这一层。 +2. **腿的价格来自可操纵的曲线。** 执行价报价(q#600=A)能防住开仓前一刻的一次性曲线操纵,但一条 + 单薄的帕里姆图曲线仍然不是一个诚实的概率。对着攻击者能影响的价格源做固定赔率,意味着池子为 + 别人对价格源的控制买单。 +3. **让现有的一个池子前置产品(杠杆,F1/#300)变得安全花了多大代价。** 杠杆是池子前置资金的 + 唯一地方,而它恰恰产生了这一失败类别:仓位利润超过输家池,缺口落到 LP 头上。它已经**被解决** + ——提前退出奖励上限加结果相关的递延权益给出 `winners_pool ≥ (1−cap)·losers − fees ≥ 0`,即 + `uncovered == 0` **在构造上成立**,LP 收取路径只作为 loud 不变量违规日志之后的防御性回退保留。 + 关键是那份保证的代价:杠杆是一笔*有界的、有抵押的*贷款,带清算扫描,而且仍然需要一个专门的 + cap、一个递延权益设计和一个常开的不变量来约束。过关簿有乘法赔付、没有可清算的抵押品、也没有 + per-leg 边界可以去封顶——同样的保证无处挂靠。 +4. **共识复杂度 vs 一个 UX 特性。** 十个新的中位数参数、新对象、新的结算路径(void 重定价、争议 + 交互、escrow FIFO)——全部都是 mainnet 之前要审计的金钱路径攻击面。 + +过关簿只有在做市商是一个中心化、完全受信的一方时才成立。那显然不是这个协议的信任模型。 + +## 取代它的是什么 + +- **优惠券**(一笔交易、N 个独立的 `pm_place_bet`)已经随 Forecaster 客户端发布——一笔无对手方的 + 多腿投注。 +- 客户端侧的 **auto-roll**(「顺序过关」)可以零共识改动地给出串关的感觉:客户端在一条腿结算后, + 把它的盈利再押到下一腿上。对手方 = 普通的帕里姆图池子;赔率不预先固定,这在帕里姆图定价下是 + 诚实的。将来如果想要链上执行保证,可以用一个小的「市场 X 结算后下注」条件操作来加固。 +- 如果将来出现一个完全受信的市场做市商,过关簿可以作为一个**独立的 opt-in 风险基金**运行 + (明确地*不是* Lazy Pool),存款人知情地接受庄家风险。在那之前不在范围内。 + +--- + +# 归档设计(否决之前,2026-08-18) + +这条线以下的内容记录了被否决之前的设计,包括 q#600/q#601 这些在它还是候选者时锁定的范围决策。 +仅供参考保留——其中没有任何一项是计划中的工作。 + +## 问题 + +随 Forecaster 客户端发布的优惠券(一笔交易携带 N 个独立的 `pm_place_bet` 操作)是一个*多腿投注*, +不是过关:每条腿各自结算,输赢相互独立。**真正的过关(accumulator/экспресс)** 是对 N 个结果的 +*合取*下的单笔押注:只有**每**条腿都赢才赔付,而潜在赔付是各腿赔率的乘积。**系统投注「M 选 N」** +是标准泛化:押注被拆分到所有 C(N,M) 个 M 腿子过关上,所以票可以存活多达 N−M 条输腿。 + +帕里姆图市场没有固定赔率——一条腿的最终系数只有在其池子关闭时才知道。所以一个天真的「把最终 +帕里姆图系数相乘」的过关无法由各腿自己的池子提供资金:跨市场的合取赔付不由任何单个市场的输家 +支撑。过关需要一个显式的对手方和一个在投注时刻固定的价格。 + +## 设计概要 + +- **对手方:Lazy Pool** —— 同一个已经在前置杠杆贷款的持货基金。过关是对着池子按曲线价格的一笔 + 边注;它**不**触及各腿的曲线或池子。 +- **投注时刻固定价格**,取自每条腿的实时曲线(二元用 CPMM,多元用 LMSR-softmax):组合价格 + `P = Π p_i`,潜在赔付 `W = S · (1 − pm_parlay_margin) / P`,带封顶。 +- **全有或全无结算**,由各腿的常规预言机裁定驱动:任何一条腿输 → 票立即死掉;一条作废(no-contest) + 的腿被*排除*(它的 `p_i` 被乘回去——庄家标准做法);所有剩余腿都赢 → 在最后一条腿结算后,池子 + 自动支付 `W`。无 claim 操作,与 `pm_payout` 自动赔付的理念一致。 +- **最坏情况 escrow**:池子在开仓时锁定 `W − S`,所以每张开着的票在构造上都被完全资金支持;押注 + `S` 立即进入 `pool.free_balance`。 + +## 机制 + +### 开仓:`pm_parlay_open` + +``` +pm_parlay_open { + account, + legs: [ { market_id, side (binary) | outcome_index (multi) }, ... ], + amount, // 押注 S,流动 VIZ + min_payout, // 对 W 的滑点保护(曲线可能在报价与纳入之间移动) + extensions +} +``` + +验证 / 求值门槛(全部是 loud `FC_ASSERT`): + +1. `2 ≤ legs.size() ≤ pm_parlay_max_legs`;所有 `market_id` 互不相同。 +2. 每条腿的市场:status 1(active),下注仍开放,且距该腿的 `betting_expiration` 至少有 + `pm_parlay_min_time_left` 秒(抗狙击:过关按实时曲线定价,所以对一条几乎关闭的腿的晚盘是 + 最便宜的攻击)。 +3. 每条腿的市场允许即时下注(`allow_instant_bet`),**未**被隐藏在预言机 risk-floor 之下,且其 + 曲线深度通过操纵门槛(见下)。 +4. 中位数 kill-switch `pm_parlay_enabled` 开启;池子有容量(见下)。 +5. `S ≥ pm_min_bet`;账户有流动的 `S`(与 `pm_place_bet` 相同的资金规则)。 + +**腿价 `p_i`** 是**腿的比例虚拟大小的执行价**,而不是 mid:对着该边/结果上的一笔假设的 `S` 即时 +下注给曲线报价,使用得到的平均价格。Mid 报价把价差免费送给攻击者;执行价让在开仓*之前*移动一条 +单薄曲线先付出移动者自己的滑点。虚拟报价**不**改变曲线。 + +**组合赔付**: + +``` +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) // 用户滑点保护 +FC_ASSERT(W > S) // 一笔不可能盈利的过关是误点,拒绝 +``` + +**开仓时的资金(单次平衡移动,守恒精确)**: + +``` +account.balance -= S +pool.free_balance += S +pool.parlay_fund_used += (W − S) // 最坏情况 escrow,由上面的断言 W − S > 0 +pool.free_balance -= (W − S) +``` + +容量门槛:`parlay_fund_used + (W − S) ≤ free-only base × pm_parlay_fund_percent` —— owner 为杠杆 +固定的同一 free-only 基数规则(q#566=A):义务只对着 `free_balance` 衡量,从不针对 NAV。 + +### 对象 + +``` +pm_parlay_object { + id, account, + legs: [ { market_id, side, outcome_index, price_ppm, // p_i 开仓时固定,百万分之一 + 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 → parlay id)**,这样 per-market +裁定无需扫描就能找到受影响的票。per-market 扇出由 `pm_parlay_max_open_per_market` 限制(开仓时通过 +有界索引探测强制,无计数器,见 commit-cap 先例 M4 和 computed-vs-counter 规则)。 + +### 结算 + +挂到同一个已经完成赔付的 per-block `process_pm_markets()` 遍历里——过关的腿对腿市场达到 +**settled** 状态(争议宽限期之后)做出反应,而不是对原始 resolve 反应,所以争议反转被自动尊重: + +- **腿输** → 票 `status = 2` 立即:释放 escrow + (`parlay_fund_used -= (W − S)`,`free_balance += (W − S)`)。押注已经躺在池子里——它就是池子 + 在输票上的收入。发射虚拟操作 `pm_parlay_lost`。 +- **腿作废**(no-contest / missed-resolution void)→ `state = 3`;赔付收缩: + `W' = W · p_i`(把被排除腿的价格乘回去),钳制 `W' = max(W', S)`;释放 escrow 差额。如果**所有** + 腿都作废 → 退还 `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 相对 pre-open 净 `−S`——恰是池子在一张赢票上的损失)。`status = 1`,发射 + `pm_parlay_won`(用于 account_history 的 per-account 虚拟操作)。 + +每个已结算市场的工作是有界的:至多触及 `pm_parlay_max_open_per_market` 张票,每张 O(legs) ≤ +`pm_parlay_max_legs`。无无界 per-block 循环(审计类别 H3/M3)。 + +**不变量**(debug 断言,像 TOKEN 锚那样做快照导入校验): + +1. `parlay_fund_used == Σ_open (W_i − S_i)` —— 可通过遍历开着的票重新计算。 +2. `pool.free_balance ≥ 0` 恒成立(FIFO 队列规则未动;过关赔付走同一「永不为负」纪律——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——把最坏情况结算工作和 escrow 数学保持得简单有界)。 +语义:押注 `S` 拆成 `C(N,M)` 等份子押注,每个子过关从同一固定 `price_ppm` 集合按上述方式定价/封顶; +escrow = 对所有组合求和。存储为一个对象(腿 + M + per-combo 派生数据在结算时计算,不存储)。 +「7 选 8」= M=7, N=8, 8 个组合。结算:最后一条腿结算时,数 won/void 腿,算术枚举组合(无递归), +支付赢的组合的赔付之和。refund/void/shrink 规则按组合应用。推迟到**实现第二阶段**,但现在就规定好, +以免对象布局和参数来回折腾(snapshot-layout 教训:批次 B → 只能靠快照重新部署)。 + +## 对抗性评审(实现前) + +| 攻击 / 失败类别 | 这里的向量 | 本设计中的缓解 | +|---|---|---| +| 曲线操纵(主要) | 拉升一条单薄腿的曲线,按扭曲的 `p_i` 买过关,平仓 | 执行价报价(移动者自付滑点),每条腿的 `pm_parlay_min_depth` 门槛(最小曲线流动性),`pm_parlay_margin` 庄家优势,硬上限 `max_payout`/`max_multiplier`,`min_time_left` 窗口 | +| 无界累积(#141 类) | `parlay_fund_used` 增长,稍后被减 | 每次终态转换释放 escrow,可重算的不变量 1,失配时钳制到 0 并 loud ilog | +| 符号翻转 / 下溢 | `W − S`、`W' = W·p_i` 收缩、退款 | 开仓时断言 `W > S`;void 收缩钳制在 `S`;所有减法钳制 `max(x,0)` + debug 断言 | +| 缺失下限/断言 | 「escrow 在构造上覆盖赔付」 | 每个维护区块对不变量 1 显式 debug 断言 + 快照导入重查(锚模式) | +| DoS / 每区块工作 | 一个市场上许多票;许多腿 | `max_open_per_market`(有界索引探测)、`max_legs`、`max_combos`,结算 O(tickets×legs) 有界 | +| 治理极端值(F3 类) | 中位数设 margin=0 / multiplier=10^9 | **每个**新参数的 `validate()` 边界(margin ≤ 20%,multiplier ≤ 10000×,legs ≤ 16,combos ≤ 1024,百分比参数 bp 检查 ≤10000)——并且每个参数**接入中位数循环**(retention 参数教训) | +| 预言机/争议交互 | 争议结算前先支付,然后反转 | 腿只对 *settled*(宽限后)状态反应,与 `pm_payout` 扫描同一截止 | +| 自我交易 LP | 下注者同时也是池存款人 | 无需特殊路径:池 P&L 与杠杆完全一样被社会化;margin + 上限约束抽取 | +| 快照往返 | 新对象/字段在导入时丢失 | 全反射导出;带 `contains()` 保护导入;forward-only 计数器要么播种、要么可重算(不变量 1 可重算——优先) | + +## 新治理参数(chain_properties,下一个版本号) + +`pm_parlay_enabled`(kill-switch,默认 **off** —— 杠杆先例), +`pm_parlay_margin`(bp,默认 500 = 5%,边界 ≤ 2000), +`pm_parlay_max_legs`(默认 8,边界 2..16), +`pm_parlay_max_multiplier`(默认 1000×,边界 ≤ 10000), +`pm_parlay_max_payout`(VIZ,默认 100k), +`pm_parlay_fund_percent`(池 free 的 bp,默认 2000,边界 ≤ 5000), +`pm_parlay_min_depth`(VIZ,默认 1000), +`pm_parlay_min_time_left`(秒,默认 3600), +`pm_parlay_max_open_per_market`(默认 1000,边界 ≤ 10000), +`pm_system_max_combos`(默认 256,边界 ≤ 1024)。 + +这十个都必须出现在:带边界的 `validate()`、中位数投票循环、`get_pm_chain_properties`、 +序列化器(C++ ⇄ js ⇄ php ⇄ python lock-step——P1 的 vop/param drift 教训),以及 +`chain_properties_pm` 的快照导出/导入。 + +## 客户端表面(节点落地后) + +优惠券界面增加一个模式切换:**Multi**(今天的 N 个独立下注)/ **Экспресс**(一个 +`pm_parlay_open`)/ **系统 M 选 N**(第二阶段)。优惠券已经以完全正确的形态收集腿;过关报价 +(`Π p_i`、潜在赔付、上限)可以从下注表单使用的同一曲线读数在客户端计算,以 `min_payout` 作为 +滑点保护。读 API:`get_account_parlays`、`get_market_parlays`(按 q#383=A 默认 newest-first), +活动里的过关卡片(历史/进行中标签)。 + +## 决策日志 + +- **q#600=A(2026-08-18):** 腿价 = 押注虚拟大小在实时曲线上的执行价(非 mid)——曲线操纵者先 + 付出自己的滑点。 +- **q#601=A(2026-08-18):** 第一轮 = 二元腿 + 简单过关;M-of-N 系统和多元(LMSR)腿是第二阶段。 + 系统的对象布局仍在上文规定好,以免状态形态在轮次之间折腾。 +- 新 `pm_parlay_*` 参数的发布默认值(margin 500 bp、max_payout 100k VIZ、max_multiplier 1000×、 + max_legs 8、kill-switch 默认 **off**)按提案保留,除非 owner 在实现前覆盖具体值;无论如何它们在 + 发布后都可中位数投票,默认值只是给最初的中位数播种。 diff --git a/@l10n/zh-CN/docs/prediction-markets/settlement-work-bounds.md b/@l10n/zh-CN/docs/prediction-markets/settlement-work-bounds.md new file mode 100644 index 0000000000..268ca0d9f8 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/settlement-work-bounds.md @@ -0,0 +1,455 @@ +# 限制每区块的结算工作(#432) + +状态:修复 **A** 已实现(默认值确认为 1.000 VIZ),修复 **D** 已完成——垃圾回收、结算和作废退款 +都在一个计量的行预算上运行。本文记录问题、权衡过的选项,以及为什么链条两个都要。 + +姊妹内部规范:[early-exit-deferred-claim](./early-exit-deferred-claim.md)、 +[specification](./specification.md) §5(crons)。 + +## 1. 漏洞 + +`pm_processing_cap_per_block`(中位数投票,默认 200)是 PM cron 的唯一限制器。它计的是**市场**, +不是工作: + +``` +while (it != idx.end() && ... && done < cap) { // §5 auto-payouts + settle_market(*this, mkt); // 触及市场的每一条下注行 + ++done; // ...而只花恰好一个 cap 单位 +} +``` + +`settle_market()`(`libraries/chain/pm_process_markets.cpp`)遍历整个 `by_market` 下注范围, +构建赢家/输家向量,通过 `adjust_balance` 支付每一行,翻转它的状态,并为每一行推入一个虚拟操作。 +没有游标也没有 resume:一个市场要么整个在一个区块内结算完,要么根本不结算。同样的形态出现在 +`gc_market()`(在一个区块内丢弃一个市场的整个对象簇)和 void/no-contest 分支中。 + +没有任何东西限制一个市场可以携带的行数: + +* 每个 `pm_place_bet` 创建一个**新的** `pm_bet_object` —— 没有按(account, market, outcome)聚合; +* 即时路径**完全没有最低下注**(`amount > 0` 和 `tokens_out > 0` 是仅有的门槛),所以一行只花 + 1 raw = **0.001 VIZ**。已在测试网上通过实时广播验证,带拒绝对照和接受对照,在 + 0.001 / 0.002 / 0.010 VIZ 下; +* 一次**部分**的 `pm_transfer_position` 把一行拆成两行,完全不需要任何押注成本——比下注还便宜, + 而且绕过了任何下注侧的下限; +* 同一类上限在其他地方早已存在——`MAX_PM_DEFERRED_CLAIMS_PER_MARKET`、 + `MAX_PM_DISPUTE_VOTES_PER_MARKET`、`MAX_PM_OPEN_COMMITS_PER_MARKET`,全是 10 000。下注行是这一类 + 里唯一被留下的敞口。 + +这不需要攻击者。一个仅仅**热门**的市场就会撞进去:在测试网上,一个玩具机器人每十分钟从三个账户 +下注,已经在一个市场上累积了 734 行(另外两个是 571 和 552)。一个有数千参与者的 mainnet 市场要大 +好几个数量级,而所有这些工作都落在争议宽限期到期的那个单一区块里。 + +## 2. 选项 + +| | 修复 | 限制工作? | 成本 | +|---|---|---|---| +| A | 即时路径上的最低下注(`pm_min_batch_bet` 的镜像) | 否——只是给行定价 | 一个断言,中位数可调 | +| B | 每个市场的行数硬上限 | 是 | 热门市场停止接受下注:审查 / 破坏 UX | +| C | 按(account, market, outcome)聚合下注 | 受账户数限制 | 侵入式:破坏 per-bet `weight`、`time_penalty`、`entry_liquidity`、transfer_position、F1 权益 | +| D | 增量结算:每区块有界行数,市场上游标 | 是 | 最大的共识差异 | + +**选定:A + D。** + +* **仅 A 不是修复。** 它把一行的价格抬高三个数量级(0.001 → 1.000 VIZ)且可投票,但它给出的边界是 + 经济性的,不是结构性的:1 000 000 行 × 1 VIZ = 1 000 000 VIZ,这是很多钱,但不是不可能的金额 + ——而且它是*质押*的,不是花掉的,所以很大一部分会在赔付时回来。更重要的是,A 对合法情形完全 + 无能为力:一个真正热门的市场不是垃圾,也不该被惩罚,可它恰恰是同一个区块时间问题。 +* **仅 D 也不够。** 它让每区块的工作有限,但行仍是免费的,所以垃圾发送者仍能把一个市场的结算拉伸 + 到数千个区块,迫使每个节点扛着状态。A 是廉价的经济护栏,让 D 的队列保持短。 +* **B 被否决**:在一个表现良好的市场上拒绝下注是产品对用户可见的失败,而且上限值还得靠猜。 +* **C 被否决**:为同样的收益,它是比 D 更宽、更冒险的差异,而且它摧毁了结算数学和提前退出权益 + 所依赖的 per-bet 属性。 + +## 3. 修复 A —— 最低下注(已实现) + +`chain_properties_pm` 增加了两个中位数投票参数: + +* `pm_min_bet` —— 默认 `1.000 VIZ`,治理下限 `0.1 VIZ`(`validate()`),是 `pm_min_batch_bet` 的 + 镜像; +* `pm_settle_rows_per_block` —— 默认 `2000`,范围 `[100, 100000]`,被修复 D 消费。 + +两者都接入了 `database.cpp` 的中位数循环。一个 PM 参数,如果声明了、反射了、验证了却从不进入这个 +循环,就会默默不可投票、冻结在代码默认值上——`pm_closed_market_retention_sec` 已经发生过一次。 + +强制点(`libraries/chain/pm_evaluator.cpp`): + +1. `pm_place_bet`,`mode == 0`(即时)→ `amount >= pm_min_bet`; +2. `pm_place_bet`,`mode == 1`(排队批量)→ `amount >= pm_min_batch_bet`。这条路径也创建行,而且 + 也没有下限——此前只有 *commit* 路径被覆盖; +3. `pm_transfer_position`,部分转移 → **转移出去的部分**和**剩余部分**都必须保持在 `pm_min_bet` 或 + 之上。低于下限的仓位不会被困住:它仍可被整体转移,那是移动行而不是拆分它。 + +4. `pm_add_liquidity` → `pm_min_liquidity`。流动性是第四个行来源,也是最初漏掉的那个:每个调用都铸 + 出自己的 `pm_liquidity_object`(贡献不按提供者合并),而求值器只断言 `amount > 0`,所以在下注路径 + 被加上下限的同时,行仍可以每 1 raw 一条地铸出。这个下限与创建市场时已经生效的下限相同,所以 + 投入流动性的最低门票不取决于你是开新市场还是事后加码——这是一个刻意做出的产品决策,而非默认。 + +## 4. 修复 D —— 增量结算(设计) + +市场携带自己的结算游标,而 cron 花一个**全局**每区块行预算(`pm_settle_rows_per_block`,所有正在 +结算的市场共享,最老的市场优先)。`settle_market` 变成一个逐区块恢复的阶段机: + +| 阶段 | 每行工作 | 受预算约束 | +|---|---|---| +| 1 force-close | 关闭结算时仍开着的杠杆仓位 | 否——见下 | +| 2 aggregate | 退还排队的行(status 5/6),加总 `losers_sum` 和赢家权重 | 是 | +| 3 claims | 从有界桶里支付结果相关的提前退出权益 | 是 | +| 4 payout | 支付赢家 / 翻转输家,每行一个虚拟操作 | 是 | +| 5 finalize | 费用、LP 结算、尘埃、`payout_status = 3`、`finalized_time` | 否——见下 | + +只有下注遍历被计量,因为只有下注行是廉价的。阶段 1 和 5 遍历杠杆仓位和流动性行,而每一条*这些* +行创建时花 `pm_min_liquidity`(100 VIZ)——是下注行的一百倍。它们的工作受经济约束,受攻击者为创建 +这些行所必须质押的约束,所以给它们计量需要为一条修复 A 已经定价掉的威胁添加游标和 resume 状态。 +如果这一点将来改变(出现更便宜的铸 LP 或杠杆行的方式),这些阶段就需要同样的处理和同样的 `escrow` +纪律。 + +#### resume 状态住在哪里 + +把当前的 `settle_market()` 从头到尾走一遍,就能得到一次暂停的结算必须携带的确切状态,而它不止是 +一个游标:资金拆分是一个两遍算法。第一遍产生聚合(`losers_sum`、总赢家权重),第二遍把它们变成 +逐行赔付。在任何区块边界切断函数,两遍都需要保留各自的部分结果,外加最终化所需的用于尘埃的 +累计总量。 + +这个状态**不**放在 `pm_market_object` 上。那是 12 个字段,每个存在过的市场都要携带,而只有正在 +结算的那少数几个用得上——这个对象已经约 40 个字段宽,而市场是链上数量最多的对象。相反,当一个 +市场进入结算时创建一份 `pm_settlement_object`,按市场唯一键控,在最终化时移除,所以成本与 +*在途*的结算成正比: + +| 字段 | 阶段 | 含义 | +|---|---|---| +| `phase`, `cursor` | 全部 | 当前阶段及其中下一个要处理的行 id | +| `stake_total` | 2 | `losers_sum`(正常)/ 总活跃押注(void) | +| `weight_total` | 2 | Σ 赢家曲线权重,如 `compute_settlement` 中为 128 位 | +| `winners_pool`, `uncovered` | 设在 3→4 | 权益最终确定后的拆分常量 | +| `distributed` | 4 | Σ 已付利润——最终化把 `winners_pool − distributed` 作为尘埃路由 | +| `lp_bonus` | 4 | Σ 从赢家收取的时间惩罚 | +| `paid_claims` | 3 | 从有界提前退出桶中取出 | +| `escrow` | 全部 | 带符号的守恒累加器(见下) | + +每赢家的赔付只取决于 `winners_pool`、`weight_total` 和行自身的字段,所以阶段 4 不需要记住它已经 +支付过的行——这正是让切断变干净的原因。void 路径复用同样的字段(它的两次按比例分配累计在 +`distributed` 和 `lp_bonus` 里),让「最后参与者吸收余数」的四舍五入与今天保持一致。 + +因为对象在最终化时被移除,一个*不在*结算的市场根本没有结算行,垃圾回收会把它和簇的其余部分一起 +丢掉。 + +让它安全的规则: + +* **确定性。** 阶段、游标和累加器住在结算对象里;预算是中位数投票参数。因此每个节点在相同的 + 区块处理完全相同的行。 +* **市场在结算期间对一切封闭。** `payout_status = 4`(「settling」)阻止 §5 重入,把 + `pm_dispute_create` 挡在外面(它要求 `payout_status == 1`),而且 GC 无法触发,因为 + `finalized_time` 只在阶段 5 盖章。 +* **每个区块边界的守恒。** 从一行释放出来但尚未支付的钱放在一个显式的 `escrow` 累加器里:行释放时 + `+= amount`,支付某人时 `-= payout`。PM 供给不变量把它计为 PM 持有,所以在结算中途截取的快照 + 精确平衡;最终化断言它归零。累加器是**带符号的**:阶段 3 从一个其行仍站着的输家池里支付提前 + 退出权益,所以在阶段 4 释放这些行之前,它合法地变为负值。这不是赤字——代币在真实账户余额里,而 + 将资助它们的行仍被计为 PM 持有,所以不变量的两侧无论如何一起移动。 +* **进展。** 一个有 N 行的市场在约 N / budget 个区块内完成;预算上 100 的下限使饿死不可能。 +* **行自身不能移动。** 每个创建、拆分或删除下注行的操作——`pm_place_bet`、`pm_commit_bet`、 + `pm_reveal_bet`、`pm_cancel_bet`、`pm_transfer_position`——断言 `mkt.status == 1`,而一个正在结算 + 的市场处于 status 3。所以阶段 2 遍历的集合恰是阶段 4 支付的集合,无需添加门槛。 + +有一个横切交互**不会**自动成立,实现必须收掉它:cron §1(未揭示承诺的罚没)把罚金加到该承诺所属 +*不管哪个*市场的 `forfeit_pool` 里,不看它的状态。今天这无害——§1 在同一区块里比 §5 更早运行,所以 +结算读到的是最终的 `forfeit_pool`——但一个跨越区块的结算可能在阶段 3 已经把 `forfeit_pool` 折进 +`winners_pool` *之后*,又长出一个 `forfeit_pool`,那些代币于是不属于任何人(孤儿直到 GC 烧掉,即 +又是 drift-400 失败模式)。时序让它在实践上不太可能——reveal 截止日在投注关闭时,远早于 +`result_expiration + grace`——但「实践上不太可能」恰恰是产生早先 drift 的推理。因此阶段 5 必须路由 +任何中途出现的 `forfeit_pool`,而不是假设它为零,最终化时的 escrow 断言必须考虑到它。 + +### 4.1 已发布:有界垃圾回收 + +回收先做——它是同样无界的遍历,却没有结算算术,所以它自己就能验证预算管道。`gc_market()` 变成 +`gc_market_step(db, mkt, budget)`:它最多丢弃 `budget` 个对象,就地递减它,并且只有当整个簇(含 +市场对象)都消失时才返回 true。一个对一个区块来说太大的市场保留它在扫描头部的位置—— +`finalized_time` 永不改变——并在下一区块继续。 + +与结算不同,这里不需要游标:每个范围都在它的 `lower_bound` 处重新进入,而已被移除的行*消失了*, +所以扫描恰在它停下的地方恢复。两个细节让暂停安全: + +* `forfeit_pool` 的烧毁现在在同一步骤里归零,否则重入会在每个区块重新烧掉同样的代币,把 + `current_supply` 推到已计总和之下; +* 一个收到一半的市场是惰性的——终态(`status 3` / `payout_status 3`),所以没有操作能触达它,而被 + 丢弃的行不持有资金(下注 `2/3`、LP `3`、杠杆终态),所以供给不变量在整个暂停期间持平。 + +由 `gc_row_budget_spans_blocks`(consensus_sim)覆盖:一个 143 行的簇、预算在其 100 的下限上,必须 +花不止一个区块,且任何区块都不能丢超过 100 行。对着一个刻意无界的对照验证过——绕过预算时,同样的 +测试报告「单个区块移除了 143 行」和「1 个区块内收集完」,即修复前行为。 + +结算在区块内先于回收被服务,所以沉重的结算积压可以推迟 GC。这无害:它只是拉伸保留期,而结算是 +有限的。 + +### 4.2 已发布:增量结算 + +`settle_market()` 变成 `settle_market_step(db, mkt, budget)`,由上述 `pm_settlement_object` 驱动, +并花与回收相同的全局行预算。实现必须做对的三件事,没有一件能从设计草图看出来: + +* **「我是最后一行吗?」不能靠往前看来回答。** 今天的代码把四舍五入余数交给最后一个参与者,它是 + 通过窥探市场剩余部分来识别的。在 resume 下这种窥探既错误(已支付的行仍在范围内,只是终态)又 + 平方级。因此阶段 2 把它聚合的行数计入 `rows_total`,阶段 4 把它已支付的行数计入 `rows_done`; + 最后一行就是 `rows_done == rows_total`,O(1) 且在整个暂停期间稳定。 +* **void 分支必须释放它烧掉的东西。** 当作废市场没有剩余参与者来吸收剩余池时,剩余被烧掉——而烧掉 + 却不从 `escrow` 里减去它,会触发最终化断言。守恒记账必须覆盖销毁路径,而不仅仅是支付路径。 +* **每区块工作不能重复一次性副作用。** 工作负载计量 `markets_in_dispute_window` 在调用点递减,现在 + 它在飞行的每个区块都运行;它移进了只运行一次的分支,即市场首次进入结算时(`payout_status != 4`)。 + 与回收中 `forfeit_pool` 烧毁同一类 bug。 + +由 `settle_row_budget_spans_blocks`(consensus_sim)覆盖:一个 140 行的市场、预算在其 100 的下限上 +结算,所以必须花不止一个区块,任何区块都不能终结超过 100 行,必须在飞行中显示 `payout_status = 4`, +并最终以每行已支付、结算对象消失、下注者余额恰好移动行所记录的总和结束。对着一个刻意无界的对照 +验证过——绕过预算时,同样的测试报告「单个区块支付了 140 行」和「1 个区块内结算完」,即修复前行为。 + +### 4.3 已发布:增量作废退款 + +两条 void 路径——cron §2(错过裁定)和 §3(争议自动关闭)——有同样的洞,还多一条边: +`refund_all_bets()` 在一个区块内遍历市场的每一行*并*构建一个持有每个参与者的内存向量,因为罚没池 +是按比例共享的,而分母只有在遍历结束时才知道。 + +`refund_market_step(db, mkt, budget)` 取代两者。它在同一谓词(`status` 0/5/6)上跑两个计量的遍: +第一遍只计量(押注总和、行数),第二遍退还押注并支付每行它在罚没池中的份额。因为第一遍不改动 +任何东西,第二遍重新遍历的*恰是*第一遍数过的集合——这就是「这次作废在退款谁」如何在暂停期间仍可 +回答,而无需给行打标或持有向量。 + +市场从飞行的第一区块起就带着 `payout_status = 4`,而这个标志现在是一个门槛,而不只是显示值: + +* `pm_resolve_market`、`pm_no_contest` 和 `pm_transfer_position` 拒绝它,所以一个迟到的预言机调用 + 无法超越一个正走了一半市场的退款; +* cron §4(争议投票最终化)跨过其市场正被 §3 作废的争议; +* cron §6(批量时代结算)跳过它,所以排队的行无法在两个遍之间移动。 + +写这个的时候掉出一个排序 bug,而且它早于本次改动:旧路径在 `return_liquidity()` *之前*排干 +`forfeit_pool`,而后者强平杠杆仓位并把它们的曲线残差直接路由**回** `forfeit_pool`。那些代币于是 +骑在市场行上直到 GC 丢弃它——stranded 在 `current_supply` 里没有主人,正是 void 路由要防止的那种 +泄漏。现在流动性先返回,剩余在其后入账。 + +由 `void_refund_row_budget_spans_blocks` 覆盖:140 行、预算在其 100 的下限上、没有预言机来裁定; +作废必须跨越区块、每区块保持在预算之下、以 `status = 3`、`resolved_outcome = -1`、每行都被退款、 +下注者余额恰好上升押注额结束。 + +### 4.4 批量执行器(cron §6) + +Cron §6 在一个区块内填满排队到某市场当前时代的每一行,价格是计市场的 cap 的一个单位——与结算 bug +同一形态,而在修复 A 之后,一个排队的行花 `pm_min_bet`(1 VIZ),恰是一个下注行的成本。在一个 LMSR +市场上,每一行还要额外支付一次曲线报价,使其每行比结算*更贵*。 + +它现在从同一个共享 `pm_settle_rows_per_block` 预算里支取,按**访问的**行收费(不只是执行的—— +访问就是该区块做的工作)。两个后果来自剩余行按*时代*匹配这一事实: + +* 时代计数器只在队列排干后才推进;中途推进会让剩余行在押注已被扣款的情况下不可达; +* 因此执行器在一个遍在途时(`pm_batch_settle_bet_cursor != 0`)也**在**时代边界之外运行,而不是让 + 已经揭示的押注为下一个边界等上一整个时代窗口。 + +resume 点是动态全局属性里的第二个游标 `pm_batch_settle_bet_cursor`,由 round-robin 扫描访问到的 +第一个市场消费(它按构造就是 `pm_batch_settle_cursor`)。丢掉它——比如一个没有该字段的旧快照—— +是安全的:该遍从时代头部重来,并按状态跳过它已经执行过的行,代价是一次空闲遍历和零资金。由 +`batch_queue_row_budget_spans_blocks` 覆盖。 + +有一个护栏是承重的而非装饰性的:§6 只在 `row_budget > 0` 时进入。它最后运行,而预算是共享的,所以 +一个结算沉重的区块可能空手到达它这里;无论如何进入都会跑零次迭代然后落入 persist 步骤,而后者—— +看不到中途市场停止——会把一个零行游标写到被停靠的游标之上。下一遍会从时代头部重启,花预算重访它 +已经执行过的行。 + +### 4.5 截止扫描重读已结算市场(2026-08-19 发现,已修复) + +`by_result_expiration` 的键是 `(status, result_expiration, id)`。一个已结算市场保持 `status == 3`, +而它的 `result_expiration` 停留在过去,所以它坐在 §5 结算扫描所遍历范围的**头部**——而跳过它不花 +`cap`,所以循环从不在它上面提前停止。因此每个区块都在触及真正的工作之前重读整个已结算积压:在 +区块 82641602 的测试网快照上实测为**48 971 次迭代,其中 48 942 次是纯 `continue`**,而只有 29 个 +市场真正欠一次结算。积压受 GC 保留期(默认 5 天)限制,所以它不是泄漏——但它与周转量成正比,而 +攻击者可以直接通过创建和裁定市场来吹大它。 + +修复把索引键改为 `(status, finalized_time, result_expiration, id)`。`finalized_time` 恰好在最终化时 +盖一次章,所以 `finalized_time == 0` 表示「仍欠工作」;两个扫描(§2 错过裁定、§5 结算)都把 +`lower_bound` 落入该组,而市场在结算的那一刻离开它。`payout_status` 刻意*不*在键里——结算扫描在飞行 +中把它 1 → 4 翻转,不得移动它正在恢复的那一行。与 `by_oracle_finalized` 同一招。索引键不序列化, +所以无需快照迁移。 + +### 4.6 争议计票(cron §4,2026-08-19 发现,已修复) + +又是同一形态,就在先前各遍从未看过的那个扫描里。Cron §4 最终化投票窗口已关闭的争议;对每个争议 +它遍历被争议市场的**每张选票**来构建按押注加权的计票,并给市场计一个计市场的 `cap` 单位。一张 +选票也不是便宜的行——每张都要一次账户查找加一次 lazy-pool 存款查找,与 §5 里测得的结算行同一 +量级。 + +M3 已经把每个市场的选票限制在 `MAX_PM_DISPUTE_VOTES_PER_MARKET`(10 000),那里的注释推理说这让 +最终化遍历安全。并非如此:这个上限约束*一个*市场,而 §4 可以在一个区块内最终化 `cap` 个这样的 +市场,所以天花板是 `cap × 10 000` = 2 000 000 行——比现在任何其他扫描尊重的预算高三个数量级。填满 +它很慢(一张选票每个市场需要一个不同的账户,而 200 个争议要 200 × `pm_dispute_fee` 的 escrow),但 +选票是持久状态:成本摊在数小时的链时间里,而工作在投票窗口到期的那个单一区块里被重放。 + +与结算不同,计票**不能**被恢复:裁决需要一次拿到所有选票,而停靠部分逐结果的和意味着在争议行上 +携带一个向量。所以预算在争议*之间*强制——一个争议只在仍有预算时开始,然后按它遍历过的选票收费。 +每区块最坏情况变成 `row_budget` + 一个市场的选票上限,而不是 `cap` × 选票上限。把一次最终化推迟 +一个区块在经济上是惰性的:`pm_dispute_vote` 拒绝 `voting_end_time` 之后的选票,所以当 §4 到达时 +选民已经最终。由 `dispute_tally_row_budget_defers_next` 覆盖。 + +它与 §5、§6 共享的排序属性值得陈述一次:预算按小节顺序花掉,所以一个被 void 路径饱和的区块可以 +不给它后面的扫描留任何东西。这是刻意的——积压是有限的会排干的工作——但这意味着「我的争议多久 +最终化」受*在途总* PM 工作的约束,而不只是 §4。 + +相关的每事务成本一起被修复。`pm_dispute_vote` 过去通过在每个*新*选票上数市场的现有选票来强制选票 +上限(遍历以 cap+1 为界):每事务有界,但每个选票 O(n),填满一个市场 O(n²),而且没有任何 cron 预算 +覆盖的工作——与 M4 用 `open_commits` 从 commit 路径移除的反模式相同。计数现在住在 +`pm_dispute_object.ballots` 上,在选票行创建时递增,在投票者*修订*一行时不动(修订覆盖该行,所以 +计数器数的是行,不是票)。选票从不单独删除——GC 丢弃整个簇——所以计数器只增。 + +快照在这里需要 `open_commits` 没有的额外一步。争议在它们的选票*之前*被导入,所以一个 +`contains` 保护的键读取无法单独修复缺字段的快照:`reconcile_pm_dispute_ballots()` 在选票导入之后 +运行,让每个计数器与实际存在的行一致。它既播种旧快照(键缺失 → 0 → 重建),也抓住新快照中的 +drift,代价是对一个导入反正刚遍历过的索引再走一遍。由 `dispute_ballot_counter_matches_rows` 覆盖, +它在每张选票之后把计数器与活的行数对照,并钉住修订路径。 + +### 4.7 小节顺序就是优先级顺序(cron §8,2026-08-19 发现,已修复) + +`process_pm_markets()` 的每个小节都对着同一个 `pm_processing_cap_per_block` 收同一个计数器 +`done`。这让小节顺序成为优先级顺序,这对做真实工作的扫描是刻意的——但这也意味着一个可靠耗尽 +预算的小节把它后面的一切变成死代码。 + +第 7 小节,lazy-pool 召回步骤,正是这样一个小节。它每个区块从头部遍历 status-0 分配索引,并对 +**它访问的每一行**收 `done`,包括那些它只检查并保持不动(`idle, steps remain, but this step isn't +due yet`)的行。对检查收费是刻意的——正是这保持小节有界——但工作集又大又长寿:在测试网区块 +82646702 上有 **34 548** 个 status-0 分配,对上 **200** 的上限。所以循环总是跑到 `done == cap`。 + +在它后面坐着第 8 小节,封禁过期扫描。它从未执行。临时的预言机和创建者封禁永远保持一个陈旧的 +`banned_until`,且 `pm_ban_expired` 从未发射。损害有限:强制比较的是 `banned_until` 与 `now`,而 +不是测试字段是否为空,所以没有账户在期满后仍被封锁——坏掉的是存储状态和历史事件,以及任何把 +「被封禁」读作「字段非零」的客户端。在这成立期间测试网上没有封禁,所以没有任何可观察的卡住; +缺陷在于该小节根本无法运行。 + +修复给扫描自己的计数器(`ban_done`),而不是移动它或放大共享上限。这安全,因为扫描是自清理的: +一次访问把 `banned_until` 置 0,这永久地把该行从被扫描范围中移出。因此每区块工作是刚刚过期的封禁 +数,而私有上限甚至约束一次同步的突发。 + +`ban_expiry_survives_saturated_cron_budget` 在微缩尺度上复现饥饿——cap 2、三个活分配来饱和它、一个 +仍必须失效的短期创建者封禁。把扫描放回共享计数器上时,测试恰在那个断言上失败。 + +它留下的通用规则:**附加到这个 cron 的新小节一到就死,除非它坐在第 7 小节之前,或携带自己的 +预算。** + +### 4.8 lazy-pool 提现队列(per-tx,2026-08-19 发现,2026-08-20 修复) + +`service_lazy_withdraw_queue()` 过去在每个调用上**整体**排干池子的 FIFO 提现队列——它循环到 +`free_balance` 耗尽——而且它从六个地方被调用,其中四个在求值器内(存款、提现、杠杆平仓、杠杆转换) +外加 cron 里的两条资本返还路径。每队列行没有下限:`pm_lazy_withdraw` 在 `owed > 0` 时对每次部分 +提现创建一个**新的**请求对象(一个 raw 就够了),同一账户的行从不合并。不对称之处在于,队列按每 +事务一行被填满,而稍后由一笔无关的事务排干——在测试网 150 k VIZ 的 free balance 下,单个调用可以 +支付多达 1.5 亿行。 + +修复(owner q#678=A):排干现在受预算约束。`service_lazy_withdraw_queue(db, row_limit)` 返回它处理 +了多少行;每事务调用点传 `1`(只付 FIFO 头部——大部队由 cron 接走),而新的 cron 第 9 小节在 +`pending_withdrawals > 0` 时把剩余排干到共享的每区块 `pm_settle_rows_per_block` 行预算为止。那个 +小节是活性后备:即使没有资本返回 `free_balance`,队列也持续以每区块最多 `row_budget` 行推进,所以 +它不会停转。cron 支付的行仍诚实地收共享预算(返回值从 `row_budget` 中减去)。 + +`lazy_withdraw_queue_row_budget_spans_blocks` 在微缩尺度上复现旧行为——250 个 one-raw 行对上 +100 行的预算——并断言队列跨越多个区块、每区块 ≤ `row_budget`、FIFO 顺序、`free_balance ≥ 0` 和 +`pending_withdrawals → 0`。用修复前的无界排干,对照在第一个区块排干全部 250 并因每区块边界失败。 +无布局变化 → 测试网无需重新部署。 + +### 4.9 清算级联按每事务运行(2026-08-20 发现,开放) + +以上所有都约束**每区块**的工作。`cascade_liquidate()` 打破了这个框架,因为它从求值器可达—— +`pm_place_bet`(两个二元分支)、`pm_cancel_bet` 和 `pm_withdraw_liquidity`——所以它的成本按 +*事务*支付,而一个区块能装下多少事务就有多少事务。 + +扫描本身的形态不可避免:每轮它遍历市场的 status-0 杠杆仓位,对着每个仓位的阈值评估 +`cancel_value()`,在第一个受害者处停下。当没有可清算的——正常情形,也是注释描述为「廉价索引探测」 +的那种——它仍会访问**该市场上的每个开仓仓位**才下结论说没有工作。`pm_min_bet`(1 VIZ)就足以触发 +一次这样的扫描,而没有任何东西限制一个区块能携带多少下注。 + +被扫集合能长多大由池经济学而非任何显式上限固定。每个开仓仓位锁住至少 `pm_min_liquidity` +(100 VIZ)的杠杆基金(#536 下限),基金是池 free balance 的 `pm_leverage_fund_percent`,而 +`free_balance` 本身随着贷款发放而缩水,所以不动点大致是 `N ≤ free / 1100`(在今天 10% 下)。一个 +持有约 11 M VIZ 的池因此支持约 10 000 个开仓仓位,而约束 3 只限制单个仓位的大小,不限制其中有多少 +个共享一个市场。按实测约 1.7 µs 每访问行,那是一个 1 VIZ 的下注买下的约 17 ms 工作,区块内每个 +下注都重复一次。 + +值得注意这个下限从哪来:`pm_min_liquidity` 是 #536 审计修复强加到贷款上的,而它自己的注释陈述了 +意图——「把全局开仓数限制到 `fund_total / pm_min_liquidity`」。这个推理对按**每区块**衡量的工作 +成立,那就是上面每一个扫描。它不适用于一个每事务跑一次的扫描:约束集合对集合被重走多少次什么也 +没说,而且没有任何东西约束重走。 + +两条诚实的限定。第一,外循环在每次清算后从范围头部重扫(K 次清算为 `O(K·N)`),但 K 自阻尼: +清算一个仓位把它的代币卖回曲线,使价格*朝向*剩余的同侧仓位移动并让它们更安全,所以大规模级联 +不是预期形态。每下注 `O(N)` 扫描才是那个不取决于任何事出问题的部分。第二,在测试网上这一切现在 +都不可达:`pm_leverage_max_per_position_bp`(20 bp)对上当前基金使每仓位上限(约 30 VIZ)小于 +100 VIZ 的贷款下限,所以根本开不了仓位——已知的 #536 冲突,owner 原样保留(q#568)。以利于小额 +贷款的方式解开这个冲突会按比例加宽这个扫描;两个决策是耦合的。 + +**决策(owner q#679=D,2026-08-20):不修。** 杠杆是小众产品,暴露的集合经济上有上限(上面的 +基金/池边界),而且攻击者反正要为级联必须扫描的每个仓位付抵押品和 funding。目标 #440 已关闭。 + +### 4.10 已检查并否决:开仓仓位扫描(cron §2c) + +下注结束即强平仓位的那一小节,每个区块通过 `by_lev_funding_due` 遍历**所有** status-0 仓位,并只 +对平仓收 `done`,所以初读像是 §4.5 再来一遍:一个完整的扫描,其被跳过的行不花预算。 + +这不是同一个缺陷,而区别值得陈述,因为它是两个家族之间的分界线。在 §4.5,范围头部被**永远不再** +需要工作的市场填满,所以空闲扫描随周转量无界增长。这里每个被扫的行是一个活的义务——一条链最终 +必须关闭的开仓贷款——而行在关闭那一刻(status 0 → 1)永久离开范围。因此工作集是与 §4.9 相同的 +经济上有界的 `N`,而不是尸体积压。让它更便宜意味着在仓位上存一个强平截止时间并为其建键索引: +为一个已经受约束的集合上的常数因子收益做布局变更和快照迁移。不值得;记录下来好让下一次审计 +不再重开它。目标 #441 以这个推理关闭。 + +### 4.11 批量执行器的空闲快速路径(cron §6,2026-08-20 发现,已修复) + +§4.4 发布了按*访问*的每一行收行预算的批量执行器,但**外**循环仍是每个边界 `O(active batch +markets)` 且不计量:一个空闲市场(当前时代无排队)在 LMSR q-vector 快照之前就跳过,什么都不付—— +没有 `done`,没有行预算——所以每边界扫描随市场*数量*线性增长,而不管实际存在多少队列。垃圾发送者 +可以创建数千个 `allow_batch` 市场,把每个时代边界变成一次完整空闲遍历。 + +修复(P0):`pm_bet` 上的 `by_status_market` 索引——`(status, market, id)`——扫描现在直接从排队 +(`status=5`)行驱动。只有真正持有队列的市场才会被访问;空闲市场从不进入范围。Resume 语义不变: +已执行行翻转 `5 → 0/2` 并离开索引,所以下一个 `lower_bound((5, market, bet))` 自然落在下一未执行行 +上,而两个动态属性游标(`pm_batch_settle_cursor`、`pm_batch_settle_bet_cursor`)保持其含义 +(市场 / 停靠行)。一个防御性跳过跨过任何泄漏到非活跃市场上的 status-5 行——§2/H2 退款已经支付 +它们,而该遍不得在计量遍和支付遍之间移动它们。目标 #445 已关闭。 + +### 4.12 递延权益清除与阶段 1/5 loud 信号(2026-08-20,已修复) + +同一次评审的两个较小残余,与 P0 一起采纳(owner 选了 C): + +- **P1 —— `purge_deferred_claims` 构建了每个递延权益的内存向量**,删除前就构建,是 + `refund_all_bets` 曾经携带(而 §4.3 已移除)的同一个参与者向量模式。权益已经被 + `MAX_PM_DEFERRED_CLAIMS_PER_MARKET`(10 000)封顶,所以这从来不是边界问题——但向量可以免费 + 丢掉:现在它在原地移除,在 `db.remove` 使其失效之前推进迭代器。目标 #446 已关闭。 + +- **P2 —— 未计量的结算阶段 1 和 5。** `settle_market_step` 只给阶段 2–4(下注行)做预算。阶段 1 + (`force_close_positions`)和阶段 5(`settle_liquidity`)整体遍历市场的每个开仓仓位 / LP 行; + 它们只受经济约束(每行 ≥ `pm_min_liquidity` = 100 VIZ)。与其给它们计量——为一个 100 VIZ 下限 + 已经定价掉的威胁加游标和 resume——代码现在在其中一个遍历单步超 `pm_settle_rows_per_block` 时 + 发射一个**loud 仅日志信号**(镜像 F1 的 `uncovered` 信号)。它是确定性的且无共识影响;它存在 + 是为了让未来降低抵押品下限(例如把 #536 朝小额贷款解开)的变化让该阶段显眼地昂贵,而不是默默 + 劣化区块时间。目标 #448 已关闭。 + +## 5. 一行到底花多少 + +用 `tests/consensus_sim/bench/settle_bench.cpp`(`make pm_settle_bench`)测得,它把行数增长的 +真实市场驱动过一条真实链,并计时结算它们的区块。Release 构建,无消毒器,无 account_history 插件: + +| 行 | 空闲区块 | 结算区块 | 每行 | gc 区块 | gc 每行 | +|---|---|---|---|---|---| +| 500 | 0.28 ms | 1.24 ms | 1.93 µs | 0.59 ms | 0.62 µs | +| 2 000 | 0.29 ms | 3.61 ms | 1.66 µs | 1.54 ms | 0.62 µs | +| 8 000 | 0.29 ms | 13.95 ms | 1.71 µs | 5.50 ms | 0.65 µs | + +结算随行数线性,**约 1.7 µs/行**,垃圾回收约 0.62 µs/行。外推:大约 **600 000 行填满一秒钟**的 +区块时间,约 1.7 M 行填满整个三秒间隔。把这当作**下**界——基准从十个账户下注(真实市场有数千个, +所以查找更不缓存友好),而模拟节点不跑 account_history 插件,所以每行推入的虚拟操作在那里几乎不花 +什么,而一个 API 节点要为索引它付费。 + +这就是单靠修复 A 的风险形态:一个有机市场远未到极限(测试网 734 行市场约 1.2 ms 结算完),但一个 +愿意每行质押 1 VIZ 的垃圾发送者可以用约一百万 VIZ 在单个区块买下约 1.7 秒的结算工作。便宜到值得 +关闭,这正是修复 D。 + +## 6. 调优 + +`pm_settle_rows_per_block` 在结算延迟与区块时间之间权衡,而上文的测量正是它应该据此设定的依据: + +* 默认 **2 000** 每区块花约 3.4 ms 的结算工作(约区间的 0.1%),并在约 500 个区块、即半小时以内 + 排干一个百万行的市场; +* 提高到 10 000 每区块花约 17 ms,并在约 100 个区块内排干同一市场; +* 100 的下限存在,好让进展始终得到保证。 + +提高 `pm_min_bet` 反而缩短队列,代价是排除小额下注者——优先先调预算。 diff --git a/@l10n/zh-CN/docs/prediction-markets/specification.md b/@l10n/zh-CN/docs/prediction-markets/specification.md new file mode 100644 index 0000000000..12e725c6e7 --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/specification.md @@ -0,0 +1,974 @@ +--- +title: Onix 协议 —— 规范 +description: Onix 协议的正式技术规范,已实现为 VIZ DLT 上的共识操作(HF14)。 +--- + +# Onix 协议规范 + +**版本:** 2.0(链上 / HF14) +**状态:** 正式技术规范 —— 现已实现为 VIZ DLT 上的共识操作 + +--- + +> **链上(HF14)。** 实现为 VIZ DLT 上的一等公民共识操作(`pm_*`),并在 `consensus_sim` 中验证。**所有 +> 百分比均为基点(bp):10000 = 100.00%**;所有时长为以**秒 / 区块**计的治理参数。中位数投票的参数位于 +> `chain_properties_pm` 结构体(§3);按市场字段为 `pm_create_market` 操作;所有状态位于 §17 的 chainbase +> 对象中。预言机费用采用 offer→quote(创建者上限 → 预言机在接受时冻结其报价,发出 `pm_market_accepted`)。 +> 争议有两种模式 —— 委员会(`dispute_mode = 0`,默认:按质押加权的**公开** `pm_dispute_vote`,关闭前可改, +> 懒惰池质押计入)与账户(`dispute_mode = 1`:指定 `dispute_resolver`)。 + +## 目录 + +1. 定义与角色 +2. 货币与精度 +3. 系统参数 +4. 市场状态机 +5. Onix Binary:恒定乘积做市商 +6. Onix Multi:LMSR 与同注分彩结算 +7. 费用结构 +8. 迟下注的时间惩罚 +9. 流动性提供 +10. 裁定与赔付 +11. 撤注 +12. 争议系统 +13. 预言机错过裁定的惩罚 +14. 预言机声誉评分 +15. 持仓转让 +16. 懒惰流动性池 +16a. 可选杠杆 +16b. 批量 / 提交-揭示下注 +17. 链上对象模型 + +--- + +## 1. 定义与角色 + +| 角色 | 定义 | +|------|-----------| +| **市场创建者** | 支付 `pm_market_creation_fee`(`pm_create_market`);设定问题、结果、流动性、费用上限与时序参数 | +| **预言机** | 注册(费用:`pm_oracle_registration_fee`),存入保险(最低:`pm_min_oracle_insurance`),在接受时以**基点**报出费用条款(≤ 创建者上限)+ 固定费,接受/拒绝市场,提供结果裁定 | +| **下注者** | 对结果下注;按本金与当前储备成比例获得代币 | +| **流动性提供者(LP)** | 向市场池提供资金;赚取流动性费的时间加权份额 + 惩罚池 | +| **懒惰池提供者** | 以锁仓期向懒惰流动性池存入 VIZ;池自动向市场分配并通过 `reward_per_share` 累加器分发奖励 | +| **争议裁决者** | 仅账户模式(`dispute_mode = 1`):按市场的 `dispute_resolver` 账户仲裁。委员会模式(`dispute_mode = 0`)不使用裁决者——由 SHARES 选民投票 | +| **DAO / 委员会基金** | 链上既有的委员会基金。接收 `pm_market_creation_fee` 与额外的预言机罚款 | + +--- + +## 2. 货币与精度 + +所有金额以整数存储,精度 = 1/1000(毫 VIZ)。`1000` 内部单位 = 1.000 VIZ。 + +时间惩罚值使用精度 = 1/1,000,000(微单位)。 + +--- + +## 3. 系统参数 + +### 中位数投票参数(`chain_properties_pm`) + +所有经济参数由代表中位数投票(无需硬分叉即可调参),位于链上 `chain_properties_pm` 结构体。每个代表通过标准 +的 **`versioned_chain_properties_update_operation`**(op ID 46)发布其偏好值 —— `chain_properties_pm` +是该版本化结构体的当前版本(v5,HF14)—— 网络对活跃代表应用**逐字段中位数**。两个风险覆盖率旋钮 +(`pm_listing_min_coverage_percent`、`pm_betting_min_coverage_percent`)属于同一个 v5 结构体,调参方式完全相同。 +**所有百分比为基点(bp,10000 = 100.00%);时长以秒或区块计** —— 但这两个覆盖率旋钮例外,它们为成交量百分比 +(100 = 1.0×)。确切默认值与范围见 +[链参数](../governance/chain-properties#pm-parameters);权威来源是结构体本身。 + +| 分组 | 参数 | +|---|---| +| 注册与下限 | `pm_oracle_registration_fee`、`pm_min_oracle_insurance`、`pm_market_creation_fee`、`pm_min_liquidity`、`pm_max_outcomes`、`pm_max_market_duration` | +| 费用与罚则(bp) | `pm_max_oracle_fee_percent`、`pm_oracle_penalty_percent`、`pm_no_contest_penalty_percent`、`pm_default_time_penalty_percent`、`pm_max_time_penalty` | +| 接受窗口 | `pm_oracle_accept_window_sec`(默认 3600 = 1 小时;未在此期限内被接受/拒绝的待定市场由 cron 作废——种子退还,创建费保留) | +| 风险 / 覆盖率(成交量 %) | `pm_listing_min_coverage_percent`(250 = 2.5×;覆盖率低于此值的市场从默认目录中隐藏,经 `show_risky` 展示)、`pm_betting_min_coverage_percent`(150 = 1.5×;建议性客户端风险确认阈值,`≤` 挂牌阈值,不在链上强制) | +| 争议 | `pm_dispute_fee`、`pm_dispute_grace_sec`、`pm_oracle_dispute_response_sec`、`pm_dispute_vote_period_sec`、`pm_dispute_auto_close_sec`、`pm_dispute_approve_min_percent`(bp)、`pm_dispute_reward_multiplier`(bp) | +| 懒惰池 | `pm_lazy_pool_enabled`、`pm_lazy_alloc_percent`、`pm_lazy_max_total_alloc_percent`、`pm_lazy_recall_step_percent`、`pm_lazy_lock_sec`、`pm_lazy_emergency_penalty_percent`、`pm_lazy_min_liquidity_fee_percent`(默认 200 = 2%;池跳过 `liquidity_fee_percent` 低于此奖励下限的市场) | +| 杠杆 | `pm_leverage_enabled`、`pm_leverage_fund_percent`、`pm_leverage_max_per_position_bp`、`pm_leverage_max_position_ratio_percent`、`pm_leverage_min_market_liquidity`、`pm_leverage_safety_margin_percent`、`pm_leverage_max_slippage_percent`、`pm_leverage_m_factor_percent`、`pm_leverage_pool_profit_percent`、`pm_leverage_expiration_buffer_sec`、`pm_conversion_profit_cost_percent` | +| 批量 / 提交-揭示 | `pm_commit_reveal_enabled`、`pm_batch_epoch_blocks`、`pm_reveal_window_blocks`、`pm_commit_no_reveal_penalty_percent`(bp)、`pm_min_batch_bet` | +| 处理 | `pm_processing_cap_per_block` | + +`pm_market_creation_fee` 与额外预言机罚款的接收方是链上既有的委员会/DAO 基金,而非 PM 专用账户。 + +### 按市场参数(`pm_create_market` 操作) + +由创建者在创建时设定;预言机费用字段为预言机在接受时据以报价的**上限**(offer→quote)。完整字段参考: +[预测市场操作](../protocol/operations/prediction-markets)。 + +| 字段 | 描述 | +|---|---| +| `oracle`、`market_type`(0 二元 / 1 多元)、`outcomes`、`url` | 市场定义 | +| `oracle_fee_percent`、`oracle_fixed_fee` | 预言机费用**上限**(bp + 固定);预言机在接受时冻结其报价 ≤ 此(且 ≤ 中位 `pm_max_oracle_fee_percent`) | +| `creator_fee_percent`、`liquidity_fee_percent` | 创建者与 LP 费用(输家池的 bp) | +| `liquidity`、`lmsr_b` | 种子流动性;`lmsr_b` 用于多元市场 | +| `betting_expiration`、`result_expiration` | 计时器 | +| `time_penalty_type`、`time_penalty_value`、`penalty_curve_type` | 迟下注惩罚形状 | +| `allow_early_resolution`、`allow_cancellation` | 开关 | +| `allow_batch`、`allow_instant_bet` | 下注模式(二元) | +| `endogeneity_tier` | 1 经济数据 / 2 体育 / 3 政治(展示/风险提示) | +| `dispute_mode`(0 委员会 / 1 账户)、`dispute_resolver` | 争议路由 | +| `dispute_penalty_percent` | 争议成立时的预言机罚则策略(bp,带符号) | +| `metadata` | 自由格式客户端 JSON(共识不透明;链下解析) | + +--- + +## 4. 市场状态机 + +### 状态 + +| Status | 名称 | 描述 | +|--------|------|-------------| +| -1 | Deleted | 预言机拒绝,**或**接受窗口(`pm_oracle_accept_window_sec`)到期;种子流动性返还创建者(创建费保留) | +| 0 | Waiting | 等待预言机审阅 | +| 1 | Active | 接受下注至 `betting_expiration` | +| 2 | Closed | 下注结束,等待预言机裁定 | +| 3 | Resolved | 结果确定,赔付已计算 | + +### 赔付状态 + +| payout_status | 名称 | 描述 | +|---------------|------|-------------| +| 0 | Not calculated | 裁定前 | +| 1 | Calculated | 赔付待处理(宽限期活跃) | +| 2 | Paid | 全部赔付已处理 | +| 3 | Disputed | 已提交争议,赔付冻结 | + +### 转移 + +```mermaid +stateDiagram-v2 + direction LR + state "Waiting (0)" as Waiting + state "Active (1)" as Active + state "Closed (2)" as Closed + state "Resolved (3)" as Resolved + state "Deleted (-1)" as Deleted + state "Paid out" as Paid + [*] --> Waiting + Waiting --> Active: 预言机接受 + Waiting --> Deleted: 预言机拒绝 + Waiting --> Deleted: 接受窗口到期 (pm_market_expired) + Active --> Closed: betting_expiration + Active --> Resolved: 提前裁定(若允许) + Closed --> Resolved: 预言机裁定 + Resolved --> Paid: 宽限窗口(12 小时),无争议 + Deleted --> [*] + Paid --> [*] +``` + +**前置条件:** + +| 转移 | 前置条件 | +|-----------|---------------| +| 0 → 1 | 预言机保险 ≥ `min_oracle_insurance`;预言机接受 | +| 0 → 1(自预言机) | 创建者 = 预言机;保险检查;创建时自动批准 | +| 0 → -1 | 预言机拒绝;种子流动性返还创建者 | +| 0 → -1(到期) | `now ≥ created_time + pm_oracle_accept_window_sec` 且预言机无动作;cron 作废市场、退还种子(创建费保留)、发出 `pm_market_expired` | +| 1 → 3 | 预言机提交带结果的裁定(0、1,或 -1 表示 no-contest);`allow_early_resolution=1` 或 `time ≥ betting_expiration` | +| 2 → 3 | 预言机提交裁定;`time ≤ result_expiration` | +| 3 → paid | 宽限期内无争议;定时任务处理赔付 | + +### 市场创建流程 + +1. 从创建者扣除 `market_creation_fee` → DAO 基金(不可退) +2. 在市场上记录来自预言机资料的 `oracle_fixed_fee` +3. 从创建者余额锁定 `liquidity` +4. 初始化储备:`reserve_a = floor(liquidity/2)`,`reserve_b = liquidity − reserve_a` +5. 计算 `k = reserve_a × reserve_b` +6. 若自预言机:经保险检查自动批准为 status=1 +7. 若外部预言机:进入 status=0 并设置 `accept_deadline = created_time + pm_oracle_accept_window_sec` + +### 预言机接受流程 + +待定市场须由其预言机在接受窗口(`pm_oracle_accept_window_sec`,默认 1 小时)内处置。三种结局: + +- **接受**(status 0 → 1):(1) 从创建者向预言机转移 `oracle_fixed_fee`(自预言机则跳过); + (2) 递增 `markets_accepted`;(3) 更新 `last_active_time`;(4) 触发懒惰池自动分配(若池有自由余额 + **且**市场 `liquidity_fee_percent ≥ pm_lazy_min_liquidity_fee_percent`)。 +- **拒绝**(status 0 → -1):种子流动性返还创建者;无 vop。 +- **到期**(status 0 → -1):若到 `accept_deadline` 仍无动作,逐块 cron 作废市场、退还种子流动性 + (**不**含创建费)、发出 `pm_market_expired`。 + +### 审计轨迹 + +每个改变状态的动作都是一个共识操作或虚拟操作,永久记录在区块日志中并可通过 `account_history` 查询。下注、 +撤注、流动性添加/提取、接受/拒绝、裁定、争议、争议裁决、赔付与罚款都作为 `pm_*` 操作/虚拟操作出现,并附带它 +们所触及的市场储备。 + +--- + +## 5. Onix Binary:恒定乘积做市商 + +### 不变量 + +``` +k = reserve_a × reserve_b +``` + +`k` 仅在流动性添加/提取操作时改变。 + +### 下注(A 侧) + +``` +new_reserve_b = reserve_b + amount +new_reserve_a = floor(k / new_reserve_b) +tokens_received = reserve_a − new_reserve_a +price = amount × 1,000,000 / tokens_received +``` + +B 侧对称(交换 a/b)。 + +### 滑点保护 + +`place-bet` 上的可选 `min_tokens` 参数。若 `tokens_received < min_tokens`,交易被拒。 + +### 市场初始化 + +``` +reserve_a = floor(liquidity / 2) +reserve_b = liquidity − reserve_a +k = reserve_a × reserve_b +``` + +最低初始流动性:100,000 mVIZ(100 VIZ)。 + +### 权重(代币)语义 + +- `weight` = 下注者获得的结果代币数量(由 CPMM 在下注时设定) +- `weight` 是**相对索取权**,而非以 VIZ 计价的赔付。结算为**同注分彩**(与 Onix Multi 相同):赢家取回本金 + 外加按权重的输家池比例份额。 +- 若下注 A 侧且结果 A 获胜:`payout = bet_amount + (weight / total_winning_weight) × winners_pool − time_penalty_on_profit` +- 若结果 A 落败:payout = 0(本金没入赢家池) + +CPMM 是**定价引擎**(概率 + 权重分配);它不再门控赔付。这使两种市场类型共享一个结算模型:*AMM 分配权重 +(二元用 CPMM,多元用 LMSR);输家按权重比例资助赢家。* + +### 价格显示 + +``` +implied_probability_A = reserve_b / (reserve_a + reserve_b) × 100% +implied_probability_B = reserve_a / (reserve_a + reserve_b) × 100% +``` + +### LP 本金保障(证明) + +在同注分彩结算下,保障精确且不依赖曲线几何: + +``` +Money OUT = L (LP principal) + Σ(winning bet_amount) + winners_pool + fees + = L + winning_bets + (losers_sum − fees) + fees + = L + winning_bets + losing_bets = L + all_bets = Money IN +``` + +无论权重如何,总赔付以 `losers_sum` 为上限,故 LP 本金 `L` 无条件返还,赢家完全由输家资助。(旧的 AM-GM 界 +`reserve_a + reserve_b ≥ L` 不再用于偿付能力;它仍是定价曲线的属性。) + +--- + +## 6. Onix Multi:LMSR 与同注分彩结算 + +### 价格函数(softmax) + +对 N 个结果,数量参数 q_1, ..., q_N,流动性参数 b: + +``` +price(i) = exp(q_i / b) / Σ_j exp(q_j / b) +``` + +**不变量:** `Σ_i price(i) = 1`(按 softmax 定义)。 + +### 成本函数 + +``` +C(q) = b × ln(Σ_j exp(q_j / b)) +``` + +在结果 i 上买入 Δ 代币的成本: + +``` +cost = C(q + Δ·e_i) − C(q) + = b × [ln(Σ_j exp(q'_j / b)) − ln(Σ_j exp(q_j / b))] +where q'_i = q_i + Δ, all other q'_j = q_j +``` + +数值稳定性(log-sum-exp 技巧): + +``` +ln(Σ exp(x_j)) = max(x) + ln(Σ exp(x_j − max(x))) +``` + +### 流动性参数 + +``` +b = S / ln(N) +``` + +其中 S = LP 补贴存款,N = 结果数。 + +### 结算(裁定时) + +``` +1. Oracle declares winning outcome +2. losers_sum = Σ bet_amount for all non-winning bets +3. oracle_fee = floor(losers_sum × oracle_fee_percent / 10000) +4. creator_fee = floor(losers_sum × creator_fee_percent / 10000) +5. liq_fee = floor(losers_sum × liquidity_fee_percent / 10000) +6. winners_pool = losers_sum − oracle_fee − creator_fee − liq_fee +7. For each winning bettor: + payout = bet_amount + (their_tokens / total_winning_tokens × winners_pool) − time_penalty +8. LP subsidy returned unconditionally +9. LP earns time-weighted share of liq_fee +``` + +### LP 本金保障(证明) + +1. LP 存入 S VIZ 作补贴。这设定 b = S / ln(N)。 +2. 下注期间,用户支付 VIZ → 取得代币。VIZ 累积为下注池。 +3. 裁定时:输家没收 100% → `losers_sum`。赢家由 `losers_sum` 支付(非补贴)。 +4. LP 补贴 S **无条件**返还 —— 它在架构上与赔付流分离。 + +### 边界情形 + +| 情形 | 结果 | +|----------|---------| +| 全部下注于获胜结果 | `losers_sum=0`、`winners_pool=0`。每个下注者取回 `bet_amount`。LP 补贴返还。 | +| 无人下注于获胜结果 | `losers_sum=total_bets`。未分配的 `winners_pool` → LP 奖励。 | +| 零成交量市场 | LP 补贴返还。无手续费、无赔付。 | +| 单一下注者获胜 | 下注者获得 `bet_amount + winners_pool`。LP 补贴返还。 | + +### 操作 + +| 操作 | 描述 | +|-----------|-------------| +| `pm_create_market_multi { oracle, outcomes, liquidity, fees, ... }` | 创建 N 结果市场 | +| `pm_place_bet_multi { market, outcome_index, amount, min_tokens }` | 为某结果买入代币 | +| `pm_cancel_bet_multi { bet_id, min_return }` | 通过反向 LMSR 卖回代币 | +| `pm_add_liquidity_multi { market, amount }` | 增加 LP 补贴(提升 b) | +| `pm_withdraw_liquidity_multi { liquidity_id }` | 提取 LP 补贴(强制最低下限) | +| `pm_resolve_multi { market, winning_outcome }` | 预言机宣布赢家,触发结算 | + +二元市场(N=2)使用 Onix Binary(CPMM)。LMSR 仅用于 N > 2。 + +--- + +## 7. 费用结构 + +### 裁定时费用计算 + +所有百分比费用在裁定时从**输家侧总成交量**计算: + +``` +losers_sum = Σ bet_amount for all losing bets + +oracle_fee = floor(losers_sum × oracle_fee_percent / 10000) +creator_fee = floor(losers_sum × creator_fee_percent / 10000) +liquidity_fee = floor(losers_sum × liquidity_fee_percent / 10000) +winners_pool = losers_sum − oracle_fee − creator_fee − liquidity_fee +``` + +下注时不从下注扣费。完整下注额进入 CPMM/LMSR 储备。 + +### 预言机固定费 + +每个市场一次性费用。由预言机在资料中设定。市场接受时由创建者付给预言机。自预言机市场完全跳过(不发生余额操 +作)。 + +### 费用追踪字段 + +- `oracle_fee_earned` —— 裁定时不使用;费用从 losers_sum 计算 +- `liquidity_fee_earned` —— 结算时实际支付给 LP 的总佣金(事后计数器,在 `settle_liquidity` 中写入;结算前及 void 市场为 0)。提前退出的 LP 不赚取佣金:`pm_withdraw_liquidity` 要求 `status == 0`,而 `earned_fee` 在结算时与 `status = 3` 一起赋值,因此提现路径永远观察不到它。结算时 LP 奖金 = `floor(losers_sum × liquidity_fee_percent / 10000) + penalty_pool + 未分配尘埃`(§7),按 amount·time 在活跃 LP 间分配。 +- 每笔下注的 `oracle_fee` 与 `liquidity_fee` 记录用于审计;不在市场上累加 + +### 取整 + +所有计算使用 `floor()`。未分配的尘额(< 1 mVIZ)在最终赔付时送入 DAO 基金。 + +--- + +## 8. 迟下注的时间惩罚 + +### 惩罚窗口 + +| 类型 | 窗口计算 | +|------|-------------------| +| Fixed(type=0) | 到期前 `penalty_window = time_penalty_value` 秒 | +| Percentage(type=1) | `penalty_window = time_penalty_value / 100 × (betting_expiration − market_creation_time)` | + +### 惩罚计算 + +``` +time_to_expiration = betting_expiration − current_time + +if time_to_expiration < penalty_window: + ratio = 1 − (time_to_expiration / penalty_window) + + if penalty_curve_type == 1: // quadratic + penalty_ratio = ratio × ratio + else: // linear + penalty_ratio = ratio + + time_penalty = floor(penalty_ratio × max_time_penalty) +else: + time_penalty = 0 +``` + +### 赔付时应用(仅作用于利润) + +``` +profit = floor(winners_pool × weight / total_winning_weight) // parimutuel share of losers' pool +penalty_deduction = floor(profit × time_penalty / 1,000,000) +net_payout = bet_amount + profit − penalty_deduction +``` + +**不变量:** `net_payout ≥ bet_amount` —— 惩罚仅作用于利润份额,故赢家始终至少取回本金。(Onix Binary 与 +Onix Multi 相同。) + +--- + +## 9. 流动性提供 + +### 添加流动性 + +``` +add_a = amount × reserve_a / (reserve_a + reserve_b) +add_b = amount − add_a +new_reserve_a = reserve_a + add_a +new_reserve_b = reserve_b + add_b +new_k = new_reserve_a × new_reserve_b +``` + +存款时记录 `sec_to_expiration = betting_expiration − current_time`。 + +### 时间加权费用分配(裁定时) + +``` +fee_pool = remaining_liquidity_fee + total_penalty_pool + +weight_i = amount_i × max(1, sec_to_expiration_i) +total_weight = Σ weight_i +fee_share_i = floor(fee_pool × weight_i / total_weight) +lp_payout_i = principal_i + fee_share_i +``` + +每笔存款为独立持仓。同一用户的多笔存款分别追踪。 + +### 提前提取 + +**前置条件:** market status=1,`time < betting_expiration`,`resulting liquidity_sum ≥ 100,000 mVIZ`。 + +``` +// Fractional withdrawal +fraction = withdraw_amount / lp_amount +withdraw_weight_a = floor(weight_a × fraction) +withdraw_weight_b = floor(weight_b × fraction) + +// Reverse reserves +new_reserve_a = reserve_a − withdraw_weight_a +new_reserve_b = reserve_b − withdraw_weight_b +new_k = new_reserve_a × new_reserve_b + +// Time-ratio discount +time_served = current_time − lp_deposit_time +market_duration = betting_expiration − market_creation_time +time_ratio = min(1, time_served / market_duration) + +// Fee share (conservative: min of both sides) +fee_from_a = floor(a_bets_sum × liquidity_fee_percent / 10000) +fee_from_b = floor(b_bets_sum × liquidity_fee_percent / 10000) +estimated_pool = min(fee_from_a, fee_from_b) − already_paid_to_early_lps +lp_tw = withdraw_amount × max(1, sec_to_expiration) +total_tw = Σ (active LP time-weights) +raw_fee_share = floor(estimated_pool × lp_tw / total_tw) +fee_share = floor(raw_fee_share × time_ratio) + +returned = withdraw_amount + fee_share +``` + +**到期后锁定:** 当 `time ≥ betting_expiration` 时禁止 LP 提取。所有 LP 持仓锁定至裁定。 + +### 提前提取的本金安全 + +提取减去原始 `weight_a` 与 `weight_b`(而非当前储备的比例份额)。若 `reserve_a < weight_a` 或 +`reserve_b < weight_b`,提取被**阻止**。 + +### 创建者作为首位 LP + +市场创建者自动成为首位 LP。其 `sec_to_expiration` 等于完整市场时长,给予最大时间权重。 + +--- + +## 10. 裁定与赔付 + +### 赔付优先顺序 + +| 优先级 | 类型 | 接收方 | 金额 | +|----------|------|-----------|--------| +| 1 | Oracle fee (2) | 预言机 | `floor(losers_sum × oracle_fee_percent / 10000)` | +| 1.5 | Creator fee (7) | 创建者 | `floor(losers_sum × creator_fee_percent / 10000)` | +| 2 | Creator LP (1) | 创建者 | `principal + time-weighted fee share` | +| 3 | LP return (1) | LP | `principal + time-weighted fee share` | +| 4 | Winner bets (0) | 赢家 | `bet_amount + floor(winners_pool × weight / total_winning_weight) − penalty_deduction`(同注分彩) | +| 5 | Dispute refund (5) | 争议参与者 | (若适用) | +| 6 | Oracle penalty bonus (6) | 全部参与者 | (若预言机被罚) | + +### 输家侧 + +赔付 = 0。本金并入储备池。 + +### 零成交量市场 + +LP 取回全部本金。预言机取得固定费(若有)。所有费用累加器保持为 0。 + +--- + +## 11. 撤注 + +### 前置条件 + +| 条件 | 检查 | +|-----------|-------| +| 下注活跃 | `bet.status == 0` | +| 用户拥有该下注 | `bet.user == current_user.id` | +| 市场活跃 | `market.status == 1` | +| 下注开放 | `current_time < market.betting_expiration` | +| 允许撤注 | `market.allow_cancellation == 1` | + +### 反向 CPMM 机制 + +对 A 侧下注(side=0): + +``` +new_reserve_a = reserve_a + tokens +new_reserve_b = floor(k / new_reserve_a) +amount_returned = reserve_b − new_reserve_b +if amount_returned <= 0: amount_returned = 0 +``` + +B 侧对称。 + +### 滑点保护 + +可选 `min_return` 参数。若 `amount_returned < min_return`,交易被拒。 + +### 状态变更(原子) + +1. 下注状态 → 1(已撤),记录 `returned_amount` +2. 更新市场储备 +3. 市场下注合计按原下注额递减 +4. 用户余额增加 `amount_returned`,`bets_balance` 减少 +5. 历史记录(type=4) +6. 带前/后储备的 market log 记录 + +--- + +## 12. 争议系统 + +### 提交前置条件 + +- 提交者在该市场下过注 +- 在裁定后 `pm_dispute_grace_sec` 之内 +- 按模式路由:**委员会**(`dispute_mode = 0`)—— 无需裁决者账户,SHARES 选民通过 `pm_dispute_vote` 投票(公开、可改至 `voting_end_time`、权重 = `effective_vesting_shares` + 懒惰池质押→shares),由 `pm_dispute_finalize` 定时任务计票;**账户**(`dispute_mode = 1`)—— 市场指定的 `dispute_resolver` 作出 `pm_dispute_resolve` +- 市场无未结争议 +- 提交者支付 `pm_dispute_fee` + +### 预言机响应 + +须在 `pm_oracle_dispute_response_sec` 内。错过则从保险自动罚没 `pm_dispute_fee`,并记录在预言机对象上。 + +预言机以 **`pm_dispute_oracle_respond`**(op ID 98)提交其反驳。由于争议是一场公开听证,该文本存储**在争议对象上** +(`oracle_response` + `oracle_response_time`,可经 `get_dispute` 读取),以便每位委员会投票者或账户裁决者在裁决 +前予以权衡。仅市场的预言机可响应,且仅在争议未结且 `now ≤ oracle_response_deadline` 时;再次提交将覆盖先前的 +响应。 + +### 争议生命周期 + +``` +Resolution (T=0) → Grace period (T to T+12h) → Dispute filed (T≤12h) + → Oracle response (12h window) → Resolver decision (up to 14 days) + → After verdict: recalculate or unfreeze → Auto-payout after new grace period + → Auto-close fallback (T+14 days): full refund + oracle penalty +``` + +### 争议成立(预言机有误 —— 翻转) + +对提出者的奖励是罚没额中切出的一份;**罚没的其余部分资助获胜下注者**(经 `forfeit_pool`),而非裁决者、也非 +DAO。**委员会投票者与账户模式裁决者均不获报酬。** + +``` +reward_target = floor(dispute_fee × pm_dispute_reward_multiplier / 10000) // bp; 30000 = ×3 +bonus = max(0, reward_target − dispute_fee), 以罚没额为上限 + +1. Disputer ← dispute_fee(托管退回)+ bonus // bonus 取自被罚没的保险 +2. forfeit_pool += (slash − bonus) // → 赢家,结算时经 winners_pool +``` + +罚没大小:委员会模式将预言机的 `dispute_penalty_percent` 按 `consensus_strength` 缩放;账户模式取裁决者的 +`penalty_amount`(均以剩余保险为上限)。 + +### 争议被驳回(预言机正确 —— 维持) + +``` +Disputer 将全部 dispute_fee → 预言机(100%,补偿)。 // 无 50/50 裁决者/DAO 拆分 +``` + +### 重算流程(预言机有误) + +1. 校验罚款(以剩余保险为上限) +2. 支付提出争议者奖励(fee + bonus);罚没余额 → `forfeit_pool`(赢家) +3. 罚没预言机保险 +5. 应用封禁(若请求) +6. 删除所有现存未支付的 payouts +7. 翻转获胜结果(A↔B) +8. 以修正后的结果从头重生成赔付 +9. 记录审计轨迹 + +### 裁决者权力 —— 封禁是合规/监管功能(仅账户模式) + +以下制裁是**账户模式** `pm_dispute_resolve`(op ID 80)的字段,由市场指定的 `dispute_resolver` 发出。当该裁决者 +是**监管方或持牌仲裁人**时,这正是其执行链下规则的方式:在单次裁决中即可罚没保险,**并将预言机与市场创建者双方** +从平台上封禁,可临时或永久。**委员会/DAO 模式(`dispute_mode = 0`)在设计上无封禁权**——公开听证只罚没保险 +(按共识强度缩放)并经 `pm_dispute_finalize` 调整声誉;它从不封禁。 + +| 参数 | 类型 | 描述 | +|-----------|------|-------------| +| `penalty_amount` | mVIZ | 额外保险罚没(0 至剩余)→ DAO 基金 | +| `ban_oracle` | 0/1 | 封禁预言机 | +| `ban_oracle_until` | unix ts / 0 | 0=永久,>0=到期 | +| `ban_creator` | 0/1 | 封禁创建者 | +| `ban_creator_until` | unix ts / 0 | 0=永久,>0=到期 | + +封禁会将发出封禁的 `resolver` 记录到目标的 `banned_by`。**解除封禁:** 同一裁决者可以 **`pm_unban`**(op ID 99, +`unban_oracle` / `unban_creator`)**提前**解除;否则封禁在 `banned_until` 到期时自然失效,此时逐块 cron 将其清除 +并发出 **`pm_ban_expired`** 虚拟操作(ID 100),以便历史/索引器观察到该解除。 + +### 自动关闭(14 天回退) + +| 动作 | 描述 | +|--------|-------------| +| 原告 | 退回争议费 | +| 预言机 | 从保险罚没 `dispute_fee` | +| 下注 | 全部退款(原始金额) | +| LP | 全部退款(仅本金) | +| 罚款分配 | 罚没额按比例分配给所有参与者 | +| 争议状态 | 置为 3(自动关闭) | + +### No-Contest 声明 + +预言机以 `market_id` 与 `reason` 调用 `oracle-no-contest`。 + +1. 所有下注 → 待退款赔付(全额原始金额) +2. 所有 LP 持仓 → 待退款赔付(仅本金) +3. 罚款:从保险扣 `dispute_fee` 的 `oracle_no_contest_penalty_percent`% +4. 罚款按比例分配给参与者 +5. 市场:`resolved_outcome = -1`,`payout_status = 1` +6. 宽限期开始(可争议) + +### 3 结果裁定(No-Contest 争议) + +裁决者从以下选其一: +- `correct_outcome = 0` —— A 胜(重算赔付) +- `correct_outcome = 1` —— B 胜(重算赔付) +- `correct_outcome = -1` —— 确认 no-contest(保留退款赔付) + +若预言机有误:待退款赔付被删除,替换为正确的赢家赔付。适用标准争议罚则。 + +--- + +## 13. 预言机错过裁定的惩罚 + +若预言机未在 `result_expiration` 前裁定: + +``` +penalty_amount = floor(oracle_insurance × oracle_penalty_percent / 100) +``` + +### 分配 + +``` +stakes[user_id] += bet_amount (for each active bet) +stakes[user_id] += liquidity_amount (for each active LP position) +total_stakes = Σ stakes[user_id] + +bonus_i = floor(penalty_amount × stakes[user_id] / total_stakes) +``` + +每个参与者获得:全额退款(本金)+ 比例奖励。 + +市场终结:status=3,payout_status=2。 + +--- + +## 14. 预言机声誉评分 + +### 原始指标(每个预言机 14 个计数器) + +| 指标 | 类型 | 来源 | +|--------|------|--------| +| `markets_accepted` | counter | oracle-accept-market | +| `markets_resolved` | counter | resolve-market | +| `markets_no_contest` | counter | oracle-no-contest | +| `markets_missed` | counter | cron(错过截止) | +| `disputes_received` | counter | create-dispute | +| `disputes_lost` | counter | resolve-dispute (status=1) | +| `disputes_won` | counter | resolve-dispute (status=2) | +| `disputes_auto_closed` | counter | cron(14 天自动关闭) | +| `dispute_responses_missed` | counter | cron(12h 响应截止) | +| `total_volume_resolved` | mVIZ | resolve-market(bets_sum 之和) | +| `total_insurance_slashed` | mVIZ | 所有罚款事件 | +| `avg_resolution_time` | seconds | resolve-market | +| `bans_received` | counter | resolve-dispute | +| `active_since` | timestamp | register-oracle | +| `last_active_time` | timestamp | accept/resolve/no-contest | + +### 派生比率 + +分母:`total_outcomes = markets_resolved + markets_no_contest + markets_missed` + +| 比率 | 公式 | +|------|---------| +| `resolution_rate` | `markets_resolved / total_outcomes` | +| `dispute_loss_rate` | `disputes_lost / disputes_received` | +| `no_contest_rate` | `markets_no_contest / total_outcomes` | +| `deadline_miss_rate` | `markets_missed / total_outcomes` | +| `dispute_response_rate` | `1 − (dispute_responses_missed / disputes_received)` | + +### 可靠性分数(0–100) + +``` +reliability_score = clamp(0, 100, + BASE_SCORE + − W_DISPUTE_LOSS × dispute_loss_rate × 100 + − W_NO_CONTEST × excess_no_contest × 100 + − W_DEADLINE_MISS × deadline_miss_rate × 100 + − W_NO_RESPONSE × (1 − dispute_response_rate) × 100 + + W_VOLUME_BONUS × volume_tier + + W_EXPERIENCE × experience_tier × freshness_multiplier + − W_BAN_PENALTY × bans_received +) +``` + +其中 `excess_no_contest = max(0, no_contest_rate − 0.10)`。 + +### 默认权重 + +| 权重 | 值 | +|--------|-------| +| BASE_SCORE | 50 | +| W_DISPUTE_LOSS | 0.40 | +| W_NO_CONTEST | 0.10 | +| W_DEADLINE_MISS | 0.20 | +| W_NO_RESPONSE | 0.15 | +| W_VOLUME_BONUS | 0–25(分层:≥10K→+5、≥100K→+10、≥500K→+15、≥1M→+20、≥5M→+25) | +| W_EXPERIENCE | 0–25(分层:≥7d→+5、≥30d→+10、≥90d→+15、≥180d→+20、≥365d→+25) | +| W_BAN_PENALTY | 每次封禁 15 | + +### 新鲜度衰减 + +| 距上次活跃天数 | 乘数 | +|----------------------|-----------| +| ≤ 30 | 1.00 | +| 31–90 | 0.75 | +| 91–180 | 0.50 | +| > 180 | 0.25 | + +### 综合信任分数 + +``` +trust_score = reliability_score × risk_factor +``` + +| 风险分(保险/下注) | risk_factor | +|---------------------------|-------------| +| ≥ 3.0× | 1.00 | +| ≥ 2.0× | 0.95 | +| ≥ 1.0× | 0.85 | +| < 1.0× | 0.70 | + +### 新预言机检测 + +`total_outcomes < 5` → `is_new = true`。UI 中独立徽章。 + +分数在读取时经 `compute_oracle_reliability_score()` 计算,不存储。 + +--- + +## 15. 持仓转让 + +### 操作 + +``` +pm_transfer_position { bet_id, to_user, amount, memo } +``` + +- 将一笔下注的全部或部分代币转给另一账户 +- 转出的代币保留原市场与结果 +- 赔付在裁定时归当前持有者 +- 无滑点、无市场冲击 —— 纯粹的记录重新分配 +- 对 Onix Binary 与 Onix Multi 持仓均适用 + +### Memo 隐私模型 + +| 模式 | 格式 | 可见性 | +|------|--------|------------| +| 明文 | 不以 `#` 开头的字符串 | 链上公开 | +| 加密 | 以 `#` 开头的字符串 | 私密 —— 仅发送方与接收方可解密 | + +加密:使用 VIZ 账户 memo 密钥的 ECIES 共享密钥 `ECDH(sender_memo_private, recipient_memo_public)`(标准 +Graphene 模型)。客户端加解密。 + +--- + +## 16. 懒惰流动性池 + +### 参数 + +| 中位数投票参数 | 作用 | +|---------|-------------| +| `pm_lazy_pool_enabled` | 池 kill-switch | +| `pm_lazy_alloc_percent` | 每个市场分配的自由余额份额(bp) | +| `pm_lazy_max_total_alloc_percent` | 活跃市场上池占比的上限(bp) | +| `pm_lazy_recall_step_percent` | 闲置市场的渐进式召回步长(bp) | +| `pm_lazy_lock_sec` | 存款锁定期(秒) | +| `pm_lazy_emergency_penalty_percent` | 紧急提取对锁定利润的罚则(bp) | +| `pm_lazy_min_liquidity_fee_percent` | 池进行分配所需的市场最低 `liquidity_fee_percent`(bp);低于它市场得不到池流动性(奖励下限) | +| `pm_min_liquidity` | 每个市场的最低分配(亦为市场种子下限) | + +### 存款 + +- 首位存款者:`shares = amount` +- 后续:`new_shares = amount × total_shares / free_balance` +- 锁定计时器:`unlock_time = now + pm_lazy_lock_sec` +- 计算份额前先结算奖励:`pending += shares × (pool.rps − user.snapshot) / PRECISION` + +### 自动分配 + +在市场激活(status → 1)时: + +``` +alloc_amount = free_balance × allocation_percent / 100 +× (1 − active_market_penalty_pct / 100) ^ oracle_active_market_count +× (1 − fault_penalty_pct / 100) ^ oracle_active_fault_stamps +``` + +奖励下限闸门(最先检查):若市场 `liquidity_fee_percent < pm_lazy_min_liquidity_fee_percent`,池**不**分配任何资金——它只补贴 LP 费用付得起的市场。此时创建者自己的种子是唯一流动性。 + +检查:`alloc_amount ≥ min_market_allocation`,`allocated + alloc_amount ≤ total × max_total_allocation / 100`。 + +Pool LP 以 `user=0` 插入。在时间加权费用分配中等同参与。 + +### 奖励分配(懒惰记账) + +在带池 LP 利润的市场裁定时: + +``` +profit = lp_return − allocation_amount +if profit > 0 AND total_shares > 0: + pool.reward_per_share += profit × PRECISION / total_shares +``` + +用户奖励(读取时计算): + +``` +live_reward = pending_rewards + shares × (pool.rps − user.snapshot) / PRECISION +``` + +### 计划提取 + +从合并的已解锁记录(全部或部分): + +``` +1. Run unlock consolidation +2. Settle rewards: pending += shares × (rps − snapshot) / PRECISION +3. Share value = shares_to_burn × free_balance / total_shares +4. Reward portion = pending_rewards × withdraw_percent / 100 +5. Total payout = share value + reward portion +``` + +### 紧急提取 + +全部存款(锁定 + 未锁定): + +``` +1. Settle rewards +2. total_value = shares × free_balance / total_shares + pending_rewards +3. profit = total_value − principal_deposited +4. if profit > 0: penalty = profit × (locked_shares / total_shares) × emergency_penalty / 100 +5. Penalty → pool reward_per_share +6. User receives: total_value − penalty +``` + +### 机会成本保护 + +> **治理 vs 硬编码。** 仅召回**步长**由中位数投票——`pm_lazy_recall_step_percent`。其余为**硬编码**,更改需 +> 硬分叉:**10 步**划分(`window/10`、`check_step ≥ 10`)、闲置判定(自上次检查起*无新下注*即为闲置—— +> `bets_sum ≤ bets_sum_at_check`),以及**每活跃市场 5% 惩罚**(`alloc × 95/100`)。故障印记的罚则与到期窗口 +> 同为硬编码。 + +**A. 渐进式召回:** 市场时长分为 **10 个固定步**。每步若自上次检查起**无新下注**,则向池召回当前分配的 +`pm_lazy_recall_step_percent`(bp,治理)。 + +**B. 活跃市场惩罚:** `factor = (1 − 5%) ^ active_market_count`——硬编码,同一预言机每个并发活跃市场递归减 5%。 + +**C. 故障印记:** 在不良市场结果(no-contest、错过截止、零成交量、败诉、无响应、自动关闭)时,预言机获得一个 +在固定洁净运营窗口后自动到期的故障印记;每个活跃印记进一步降低其分配。(罚则大小与到期窗口为硬编码。) + +--- + +## 16a. 可选杠杆(懒惰池提供资金) + +自 HF14 上线;可选,由中位数 kill-switch `pm_leverage_enabled`(默认关)治理。 + +- **开仓**(`pm_leverage_open`)—— 下注者提交抵押;懒惰池从 `free_balance` **借出**保证金(受 + `leverage_fund_used` 限制;开仓时对照 `pm_leverage_fund_percent`、`…_max_per_position_bp`、 + `…_max_position_ratio_percent`、`…_min_market_liquidity` 检查)。不增发代币 —— 从系统视角看持仓足额 + 抵押。杠杆开仓**不**创建 `pm_bet`;曲线权重持有在 `pm_leverage_position_object` 上。 +- **清算** —— 按**下注前储备**执行,故池回收 `min(cancel_value, obligation) ≥ loan`:对向下注级联 + (`pm_place_bet`)与结算强平始终全额回收(贷款 + 利息 → 池);**唯一**有界的坏账路径是同侧 + `pm_cancel_bet`(情形 B)。级联**不**受 `pm_leverage_enabled` 门控(该标志仅阻止新开仓),故关闭杠杆永 + 不剥夺已开持仓的保护。 +- **虚拟操作** —— `pm_leverage_resolve`(结算时强平,带结果 + 杠杆)、`pm_leverage_liquidate`(盘中, + `reason` 0 对向 / 1 撤注)。 +- **治理权重** —— 懒惰池存款人保留其在 PM 争议与 DAO 委员会的投票权重(池 NAV → vesting-shares,经 + `get_vesting_share_price`,HF14 门控)。 + +API:`get_account_leverage_positions`、`get_market_leverage_positions`、`get_lazy_pool`。 + +## 16b. 批量 / 提交-揭示下注(抗 MEV) + +自 HF14 起对**二元**市场上线(多元在 LMSR 批量落地前强制 `allow_instant_bet`);按市场可选(`allow_batch` +/ `allow_instant_bet`),中位数 kill-switch `pm_commit_reveal_enabled`。 + +- `pm_place_bet` 带 `mode = 1` 将一笔**批量**下注入队;`pm_commit_bet`(承诺哈希 + 托管)→ + `pm_reveal_bet` 运行**提交-揭示**流程。未揭示的承诺通过 `pm_commit_forfeit` 没收 + `pm_commit_no_reveal_penalty_percent`(bp)。 +- 在每个纪元边界(`pm_batch_epoch_blocks`,揭示窗口 `pm_reveal_window_blocks`)入队下注由 `pm_batch_settle` + 定时任务以**统一价格**结算 —— 只有净残量推动 AMM,故批内排序无优势,且 `Σ reserve ≥ L` 不变量得以保持。 + +## 17. 链上对象模型 + +所有状态都存于在 HF14 注册为核心索引的 **chainbase 对象**中 —— 不存在 SQL 数据库。字段级定义位于操作/对象头文件中,并可通过 [`prediction_market_api` 插件](../plugins/prediction-market-api) +只读查询。原型保存在 `users` 表上的声誉计数器现为 `pm_oracle_object` 的字段。 + +| 对象(索引) | 保存 | 按何查找 | +|---|---|---| +| `pm_oracle_object` | 预言机注册、保险、14 个声誉计数器、故障印记、封禁(`banned_until` + `banned_by`) | owner | +| `pm_market_object` | 市场配置、CPMM 储备(`reserve_a/b`、`k`)、`*_fee_percent`(bp)、`status` / `payout_status`、计时器、`dispute_mode`、`a_bets_sum` / `b_bets_sum`、预言机裁定声明(`decision_url` / `decision_reason`) | id / creator / oracle / result_expiration | +| `pm_outcome_object` | 每结果的 LMSR `q`、`bets_sum`、`bets_count`(多元市场) | market + outcome | +| `pm_bet_object` | 一笔下注 —— account、`side` / `outcome_index`、`amount`、曲线 `weight`、`time_penalty`、`status`、`mode` | market / account | +| `pm_liquidity_object` | 一个 LP 持仓 —— 本金、存款时间、时间权重;`provider` 为空 ⇒ 懒惰池 LP | market | +| `pm_commit_object` | 提交-揭示的承诺哈希 + 托管(批量 / 提交-揭示) | market / account | +| `pm_dispute_object` | 一个争议 —— disputer、`proposed_outcome`、费用托管、计时器、`status`、`dispute_mode`、预言机反驳(`oracle_response` / `oracle_response_time`) | market | +| `pm_dispute_vote_object` | 一张委员会选票 —— voter、`vote_outcome`、`vote_percent`(关闭前可改) | market + voter | +| `pm_lazy_pool_object` | 单例池 —— `free_balance` / `allocated_balance` / `earned_balance`、`reward_per_share`、`leverage_fund_used`、`total_shares` | 单例(id 0) | +| `pm_lazy_deposit_object` | 一位存款人 —— shares、奖励快照、解锁时间 | account | +| `pm_lazy_allocation_object` | 池对某市场的静默 LP 分配 + 渐进式召回状态(`bets_sum_at_check`、`check_step`、`recalled_amount`) | market | +| `pm_leverage_position_object` | 一个已开杠杆持仓 —— collateral、loan、obligation、曲线权重、`status` | account / market + status | +| `pm_creator_ban_object` | 被封禁的创建者 —— `banned_until`、`ban_count`、`banned_by` | ban account | + +声誉指标在读取时计算(`compute_oracle_reliability_score()` —— §14),不存储。所有百分比字段均为基点 +(`*_percent`,bp)。懒惰池的按市场分配与渐进式召回状态位于 `pm_lazy_allocation_object`;预言机故障印记与声誉计数器位于 `pm_oracle_object`。 + +**仅插件(非共识):** `pm_market_meta_object` —— 链下解析的市场元数据(类别 / 标签 / 受禁司法辖区),用于 +发现与辖区过滤;由 [`prediction_market_api`](../plugins/prediction-market-api) 插件从每个市场不透明的 +`metadata` 字符串构建,从不参与共识。 + +这些对象的完整字段定义见[预测市场操作](../protocol/operations/prediction-markets)。 diff --git a/@l10n/zh-CN/docs/prediction-markets/whitepaper.md b/@l10n/zh-CN/docs/prediction-markets/whitepaper.md new file mode 100644 index 0000000000..c17ead3f3a --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/whitepaper.md @@ -0,0 +1,725 @@ +--- +title: Onix 协议 —— 白皮书 +description: Onix 协议产业白皮书:VIZ DLT 上具备流动性保障的预测市场。 +--- + +# Onix 协议:VIZ DLT 上具备 LP 保障的预测市场 + +**产业白皮书** + +*Anatoly Piskunov (On1x)* +*版本 2.0 —— 2026 年 6 月(链上 / HF14)* + +--- + +> **链上状态(HF14)。** 本文最初针对中心化原型撰写。协议现已作为 VIZ DLT 上的**一等公民共识操作 +> (`pm_*`)**运行,并在 `consensus_sim` 中验证。自 HF14 起上线:两种市场类型(CPMM 二元 + LMSR 多元)、 +> 同注分彩零和结算、**懒惰池**、可选**杠杆**子系统、可选**批量 / 提交-揭示下注**(二元)、保证金预言机, +> 以及双模式争议系统(委员会 / 账户)。所有百分比参数均为**基点(bp):10000 = 100.00%**(原型使用千分比 +> permille)。下文在链上设计与原型文本不同之处均有标注。 + +## 摘要 + +预测市场将分散的信息聚合为价格,其概率估计持续优于民调、专家组和统计模型。然而采用仍受一个结构性问题制约: +**流动性提供者会亏钱。** + +Uniswap v3 的 LP 承受无常损失。LMSR 做市商面临全部补贴的风险。CLOB 做市商遭遇逆向选择。每一种现有模型都要求 +资本提供者以不确定的收益换取下行风险——数据显示大多数人亏损。 + +**Onix 协议**彻底消除 LP 风险。这是一种预测市场架构,其中 LP 本金由**结算机制本身结构性地保障**——不是靠 +保险,不是靠对冲。赢家只从输家被没收的本金中获得赔付。LP 资金提供市场深度,但从不用于结算下注。 + +本文描述 Onix 协议的两种市场类型——**Onix Binary**(恒定乘积做市商)与 **Onix Multi**(LMSR 定价 + +同注分彩结算)——以及市场架构、预言机与争议解决系统、懒惰流动性池、可选杠杆、治理模型,及其作为共识级操作在 +VIZ DLT 上的实现。 + +--- + +## 1. 问题:预测市场中的 LP 风险 + +每个预测市场都需要流动性。没有它,价格便毫无意义——一笔使市场移动 20% 的下注暴露的是下注者的资金,而非群体 +的智慧。根本问题是:**谁提供这份流动性,他们承担什么风险?** + +### 1.1 当前格局 + +| 平台 | LP 模型 | LP 风险 | 收益来源 | +|----------|----------|---------|-------------| +| **Uniswap v3** | 集中流动性 AMM | 无常损失(常 >5% 年化;>50% 的 v3 LP 跑不赢买入持有) | 交易手续费 | +| **Aave / Compound** | 借贷池 | 智能合约风险、清算级联 | 借款人利息 | +| **Curve** | Stableswap AMM | 锚定资产低 IL、智能合约风险 | 手续费 + CRV 增发 | +| **Standard LMSR** | 做市商补贴 | 损失高达 `b × ln(N)`——全部补贴 | 买卖价差 | +| **Polymarket (CLOB)** | 主动做市 | 库存风险、逆向选择 | 买卖价差 | +| **Kalshi** | 无 LP 概念 | 不适用(交易所模型) | 不适用 | + +格局清晰:为预测市场提供流动性要么需要主动管理技能(CLOB),要么容忍资本损失(LMSR),要么接受无常损失 +(AMM)。这些都不适合零售参与者。 + +### 1.2 为何重要 + +预测市场在深而具流动性时表现最佳。深度市场产生准确价格、吸引知情交易者,并产生使预测市场成为公共品的信息价 +值。但深度需要资本,资本需要风险补偿。 + +结果是先有鸡还是先有蛋的问题: +- 浅市场 → 高滑点 → 差体验 → 少下注者 → 低手续费 → 无 LP 激励 → 浅市场 + +打破此循环需从等式的 LP 一侧移除风险。若提供流动性是无风险的,进入门槛降为零,飞轮便可开始转动。 + +--- + +## 2. Onix 协议 + +### 2.1 设计原则 + +Onix 协议建立在三条架构不变量之上: + +1. **LP 本金结构性安全。** 这不是风险缓释策略——而是赔付架构的属性。LP 资金与下注结算取自物理上分离的池。 + +2. **输家资助赢家。** 所有赔付(赢家利润、预言机费、创建者费、LP 费)只取自输家被没收的本金。手续费在裁定时 + 计算为 `floor(losers_sum × fee_bp / 10000)`(bp:10000 = 100.00%),从不在下注时扣除。 + +3. **双市场类型,单一保障。** 二元市场(Onix Binary)与多元市场(Onix Multi)使用不同定价公式,但共享同一 + 结算模型与同一 LP 保障。 + +### 2.2 Onix Binary(CPMM + 同注分彩结算) + +Onix Binary 使用恒定乘积做市商公式——与 Uniswap 相同的 `x * y = k` 不变量——作为二元结果的**定价引擎**, +配合**同注分彩结算**(输家按权重比例资助赢家),与 Onix Multi 相同的结算模型。 + +**机制:** + +市场维护两个储备 `reserve_a` 与 `reserve_b`,乘积恒为 `k`: + +``` +k = reserve_a × reserve_b +``` + +当用户对结果 A 下注 `amount`(实现中 side 0 → `reserve_a`)时,本金进入该侧储备,代币从**对侧**储备取出: + +``` +new_reserve_a = reserve_a + amount +new_reserve_b = floor(k / new_reserve_a) +tokens_received = reserve_b − new_reserve_b +``` + +`tokens_received`(称为 `weight`)是用户对赢家池的**相对索取权**(若结果 A 获胜;结算为同注分彩——见下, +与 Onix Multi 相同)。被下注的一侧其隐含概率上升(A 上的钱越多 → `reserve_a` 增长 → `P(A)` 增长): + +``` +P(A) = reserve_a / (reserve_a + reserve_b) +P(B) = reserve_b / (reserve_a + reserve_b) +``` + +**结算与 LP 安全性证明(同注分彩):** + +裁定时,赢家取回本金,外加按权重的输家池比例份额(与 Onix Multi 相同): + +``` +winners_pool = losers_sum − fees +payout = bet_amount + (weight / total_winning_weight) × winners_pool − time_penalty_on_profit +``` + +LP 本金 `L` 无条件返还,保障精确: + +``` +Money OUT = L + winning_bets + winners_pool + fees = L + winning_bets + losing_bets = L + all_bets = Money IN +``` + +无论权重如何,总赔付以 `losers_sum` 为上限,故 LP 资金从不用于结算下注。CPMM 是**定价引擎**(概率 + +权重);它不门控赔付。(AM-GM 关系 `reserve_a + reserve_b ≥ 2√k = L` 对定价曲线仍成立,但不再用于偿付能力。) + +**示例:** + +``` +Setup: 200 VIZ liquidity → reserve_a = 100, reserve_b = 100, k = 10,000 +Fees (bp): oracle 50 (0.5%), creator 50 (0.5%), liquidity 100 (1%) + +Alice bets 50 VIZ on A → receives weight 33.33 (price moves from 50% to 69%) +Bob bets 80 VIZ on B → receives weight 81.82 + +Resolution: A wins + Losers (Bob): 80 VIZ forfeited → losers_sum = 80 + oracle_fee = floor(80 × 50/10000) = 0.4 VIZ + creator_fee = floor(80 × 50/10000) = 0.4 VIZ + liq_fee = floor(80 × 100/10000) = 0.8 VIZ + winners_pool = 80 − 1.6 = 78.4 VIZ + + Alice (only winner, weight 33.33 of 33.33): + payout = 50 (stake) + 78.4 × (33.33/33.33) = 128.4 VIZ (minus any time penalty on profit) + LP return: 200 VIZ principal + share of 0.8 VIZ fee pool +``` + +### 2.3 Onix Multi(LMSR + 同注分彩结算) + +Onix Multi 是协议针对 3–10 个结果市场的创新。它结合 Hanson 的对数市场评分规则(LMSR,2003)做实时定价, +与同注分彩结算来保障 LP 安全。 + +**定价(LMSR softmax):** + +对结果为 {1, 2, ..., N} 的市场,每个结果有数量参数 `q_i`: + +``` +price(i) = exp(q_i / b) / Σ_j exp(q_j / b) +``` + +这是 softmax 函数——价格按构造总是恰好相加为 1.0。无需任何套利机制或 split/merge 操作。 + +在结果 i 上买入 Δ 代币的成本: + +``` +C(q) = b × ln(Σ_j exp(q_j / b)) + +cost = C(q + Δ·e_i) − C(q) +``` + +参数 `b` 控制价格敏感度(b 越高,每笔下注的价格冲击越小)。它由 LP 补贴提供资金:`b = S / ln(N)`,其中 +S 为总补贴。 + +**创新——同注分彩结算:** + +在**标准 LMSR** 中,做市商是所有下注的对手方。若群体正确预测结果,做市商损失高达 `b × ln(N)`——可能是全部 +补贴。这正是 LMSR 在企业预测市场(Microsoft、Inkling,由运营方吸收损失)之外采用有限的原因。 + +**Onix Multi 改变了赔付来源。** 裁定时: + +``` +1. Oracle declares the winning outcome +2. Losers forfeit 100% → losers_sum +3. Fees deducted from losers_sum (bp; 10000 = 100.00%): + oracle_fee = floor(losers_sum × oracle_fee_bp / 10000) + creator_fee = floor(losers_sum × creator_fee_bp / 10000) + liq_fee = floor(losers_sum × liquidity_fee_bp / 10000) + winners_pool = losers_sum − fees +4. Winners receive: + payout = bet_amount + (tokens / total_winning_tokens × winners_pool) − time_penalty +5. LP subsidy returned unconditionally +``` + +赢家由输家支付,而非 LP。补贴在架构上与结算流分离。 + +**LP 本金保障证明:** + +1. LP 存入 `S` VIZ 作为补贴,用于提供市场深度。 +2. 下注期间,用户支付 VIZ → 取得结果代币。VIZ 累积为下注池。 +3. 裁定时,输家被没收的本金资助赢家赔付与手续费。补贴 `S` 从未进入赔付池。 +4. 无论结果如何,补贴无条件返还给 LP。 + +**对比:** + +| 维度 | Standard LMSR | Onix Multi | +|-----------|---------------|------------| +| LP 角色 | 所有下注的对手方 | 深度存款(非对手方) | +| LP 最大损失 | `b × ln(N)`(全部补贴) | **零** | +| 赢家赔付 | 1 代币 = 1 单位货币 | 代币 = 对输家池的比例索取权 | +| 需要 CTF split/merge? | 是(强制价格之和 = 1) | 否(softmax 保证) | + +**示例(3 结果选举):** + +``` +Setup: b = 1000, outcomes = [A, B, C], subsidy = 1000 VIZ +Initial: price(A) = price(B) = price(C) = 33.3% + +Alice bets 50 VIZ on A → ~47 tokens (price: 33% → ~38%) +Bob bets 100 VIZ on B → ~88 tokens +Carol bets 30 VIZ on C → ~29 tokens + +Resolution: A wins + Losers: Bob (100) + Carol (30) = 130 VIZ + Fees (200 bp = 2% total): 2.6 VIZ + winners_pool = 127.4 VIZ + + Alice: 50 + (47/47 × 127.4) = 177.4 VIZ + LP: 1000 VIZ returned in full + share of liquidity fees +``` + +### 2.4 边界情形 + +| 情形 | 结果 | +|----------|---------| +| 全部下注于赢家 | `losers_sum = 0` → 每个下注者恰好取回其下注额。LP 补贴返还。零和。 | +| 无人下注于赢家 | 整个输家池未分配 → LP 奖励。LP 利润最大化。 | +| 零成交量市场 | LP 补贴全额返还。无手续费、无赔付。 | +| 单一下注者获胜 | 该下注者获得 `bet_amount + winners_pool`。LP 补贴返还。 | + +--- + +## 3. 市场架构 + +### 3.1 市场生命周期 + +```mermaid +stateDiagram-v2 + direction LR + state "Waiting (0)" as Waiting + state "Active (1)" as Active + state "Closed (2)" as Closed + state "Resolved (3)" as Resolved + state "Deleted (-1)" as Deleted + state "Paid out" as Paid + [*] --> Waiting + Waiting --> Active: 预言机接受 + Waiting --> Deleted: 预言机拒绝 + Active --> Closed: 下注到期 + Active --> Resolved: 提前裁定(若允许) + Closed --> Resolved: 预言机裁定 + Resolved --> Paid: 宽限窗口(12 小时) + Deleted --> [*] + Paid --> [*] +``` + +市场由创建者创建,经预言机(质押保险)审阅并接受,开放下注,以某一结果裁定,并在争议宽限期后赔付。 + +### 3.2 费用模型(仅从输家提取) + +Onix 协议的一个鲜明特征是**下注时不扣任何手续费**。完整下注额进入市场储备。手续费仅在裁定时计算,且只取自输 +家被没收的本金: + +```mermaid +flowchart TD + LS["losers_sum(输家下注的 100%)"] + LS --> OF["oracle_fee = floor(losers_sum × oracle_fee_bp / 10000)"] + LS --> CF["creator_fee = floor(losers_sum × creator_fee_bp / 10000)"] + LS --> LF["liquidity_fee = floor(losers_sum × liquidity_fee_bp / 10000)"] + LS --> WP["winners_pool = losers_sum − 所有费用"] +``` + +这提供了结构性保障:手续费与赢家赔付取自完全分离的资金来源。费用提取永不与对赢家的义务竞争。 + +**预言机费用条款在接受时冻结(offer→quote)。** 创建者公布预言机可收取的**上限**(bp 计的 +`oracle_fee_percent` 上限 + `oracle_fixed_fee` 上限);预言机接受时报出其实际条款(≤ 创建者上限且 ≤ 中位 +治理上限 `pm_max_oracle_fee_percent`),冻结到市场上并发出 `pm_market_accepted` 虚拟操作。自预言机在创建时 +冻结其条款。**预言机固定费**(每个市场)从输家池余额支付(从不增发)。**市场创建费**进入 DAO 基金作为反垃圾 +保护。 + +### 3.3 时间加权 LP 分配 + +LP 手续费份额按 `amount × max(1, seconds_to_expiration)` 比例分配: + +``` +weight_i = amount_i × max(1, sec_to_expiration_i) +fee_share_i = floor(total_fee_pool × weight_i / Σ weight_j) +``` + +早期 LP 每单位资本的收益远高于后期 LP。在 48 小时市场中,第 1 小时存入的 LP 每 VIZ 收益约为第 47 小时存入者 +的 ~2400 倍。 + +每笔存款作为独立持仓追踪——同一用户的多笔存款分别加权与支付。无论市场结果如何,LP 本金始终全额返还。 + +### 3.4 迟下注的时间惩罚 + +为抑制临近到期的下注(其不确定性风险更小),对临近到期下注施加可配置的时间惩罚: + +``` +if time_to_expiration < penalty_window: + ratio = 1 − (time_to_expiration / penalty_window) + penalty_ratio = ratio² // quadratic (default) + time_penalty = floor(penalty_ratio × max_penalty) +``` + +惩罚**仅作用于利润**,永不作用于本金。获胜下注者始终至少取回其原始本金。二次曲线在惩罚窗口早期平缓、后期陡 +峭,奖励「稍迟」而非「极迟」。 + +### 3.5 持仓转让 + +持仓可通过原生协议操作在账户间转让: + +``` +pm_transfer_position { bet_id, to_user, amount, memo } +``` + +无滑点、无市场冲击——纯粹的记录重新分配。`memo` 字段支持明文与加密两种模式(通过 VIZ 账户 memo 密钥的 +ECIES),可实现 P2P 交易、OTC 交易与私密备注。 + +这是 Conditional Tokens Framework(Polymarket/Gnosis)中唯一带来真实用户价值的可组合性特性。CTF +split/merge 在架构上不必要——Onix 定价公式按构造保证价格一致性。 + +--- + +## 4. 预言机与争议解决 + +### 4.1 保证金预言机模型 + +Onix 协议中的预言机默认不被信任——它们是**有保证金的**。每个预言机必须: + +- 以一次性费用注册(默认 10 VIZ) +- 存入保险(最低 5,000 VIZ) +- 显式接受市场(每次接受都质押其保险) +- 以结果及支撑证据(decision URL)裁定市场 + +保险保证金创造问责:误裁、错过截止或败诉的预言机其保险会被罚没。为使经济安全模型成立,保证金必须超过预言机潜 +在的操纵利润。 + +预言机收入有两个来源: +1. **固定费**(每个市场)——补偿质押保险与提供裁定 +2. **百分比费**(裁定时取自输家池)——随市场成交量扩展 + +### 4.2 争议仲裁 + +任何下注者可在宽限期内支付争议费以质疑裁定。争议期间所有赔付冻结。 + +裁决以两种**按市场选择**的模式之一进行,于创建时选定: + +- **委员会模式(`dispute_mode = 0`,默认)**——*全体 SHARES 选民*通过**按质押加权投票** + (`pm_dispute_vote`)决定,由 `pm_dispute_finalize` 定时任务在 `voting_end_time` 确定性地计票。这是 + **公开听证**:实时计票可查询,投票**不**隐藏于 commit-reveal 之后(一项刻意且永久的选择——DAO 尽可能 + 透明地裁决争议)。由于听证期间会浮现新证据,**投票可修改**至关闭(重复投票覆盖先前的)。投票者权重为其 + `effective_vesting_shares` **加上其转换为 vesting-shares 的懒惰池质押**,因此把 VIZ 存入池中的 DAO + 成员保留其治理权重。 +- **账户模式(`dispute_mode = 1`)**——单一指定的 `dispute_resolver`(建议多签)作出裁决 + (`pm_dispute_resolve`)。 + +任一模式下的裁决逻辑: + +**若预言机有误(翻转):** +- 应用正确结果并重算赔付。 +- 提出争议者取回其费用,**外加从被罚没保险中切出的奖励**,大小为 + `dispute_fee × pm_dispute_reward_multiplier`(bp;如 30000 = ×3),以罚没额为上限。 +- **罚没的余额加入赢家池**(经 `forfeit_pool`)——归获胜下注者,**而非**裁决者或 DAO。委员会投票者与账户 + 模式裁决者**均不获奖励**(委员会投票是无偿的治理职责)。 +- 预言机保险被罚没(委员会模式按共识强度缩放,账户模式按裁决者的 `penalty_amount`),并可附加封禁。 + +**若预言机正确(维持):** +- 提出争议者**将全部争议费没收给预言机**(对恶意挑战的补偿)。 +- 原赔付不变继续。 + +**防拒裁:** 若裁决者 14 天内不作为,争议自动关闭:所有下注与 LP 退款,预言机受罚,提出争议者的费用退回。这 +保证资金永不被无限期冻结。 + +### 4.3 预言机声誉评分 + +协议为每个预言机追踪 14 项链上指标并计算可靠性分数(0–100): + +``` +reliability_score = clamp(0, 100, + 50 (base) + − 0.40 × dispute_loss_rate × 100 + − 0.10 × excess_no_contest × 100 + − 0.20 × deadline_miss_rate × 100 + − 0.15 × (1 − dispute_response_rate) × 100 + + volume_bonus (0–25) + + experience_bonus × freshness_multiplier (0–25) + − 15 × bans_received +) +``` + +关键设计选择: +- **比率而非计数**——100 次中败 1 次(1%)优于 2 次中败 1 次(50%) +- **从 50 中性起步**——新预言机须赢得声誉,而非从 100 起步 +- **新鲜度衰减**——不活跃的预言机随时间失去经验加分 +- **成交量分层**——高成交量预言机因经过验证的记录获得加分 + +可靠性分数与风险因子(保险与下注量之比)结合,产生**综合信任分数**——展示给用户的主要指标。 + +### 4.4 No-Contest 与 3 结果裁定 + +无法核实结果的预言机可自愿宣布 **no-contest**,以较低罚则触发退款(从保险扣除 50% 争议费——远比败诉便宜)。 +这形成激励梯度: + +| 情形 | 预言机成本 | 封禁风险 | +|----------|------------|----------| +| 自愿 no-contest | 500 VIZ | 无 | +| 败诉 | 1,000+ VIZ + 额外罚款 | 永久或临时 | +| 错过截止 | 250 VIZ(自动罚款) | 无(但损害声誉) | + +若用户认为预言机滥用 no-contest,可对其提出争议。裁决者随后从**三个**可能的正确结果中选择:A 胜、B 胜,或 +确认 no-contest。这阻止预言机用 no-contest 来逃避向获胜下注者赔付。 + +--- + +## 5. 懒惰流动性池 + +### 5.1 资本部署问题 + +个体 LP 提供需要主动选择市场。多数用户不会手动评估并存入特定市场。结果:多数市场仅以创建者的初始流动性启动, +产生浅订单簿与高滑点。 + +### 5.2 池到市场的自动分配 + +懒惰流动性池通过接受存款并在每个新市场激活时**自动分配**池自由余额的一个百分比来解决此问题: + +``` +alloc_amount = free_balance × allocation_percent / 100 +``` + +分配按当前自由余额(非原始总额)计算,形成几何衰减——池永不被完全耗尽: + +``` +After 50 markets (2% allocation each): ~357 VIZ free from original 1,000 +After 100 markets: ~133 VIZ still free +``` + +最大总分配上限(默认 70%)提供额外安全。 + +### 5.3 奖励分配(一个共享累加器) + +问题:当市场带池利润裁定时,该利润须按份额比例分给**所有**当前存款人——但在每个市场上遍历每位存款人将是 +O(N) 且无界。池用**一个全局累计值** `reward_per_share`(「rps」)避免这一点: + +``` +// When a market resolves with pool LP profit, the per-share value of the pool rises once: +pool.reward_per_share += profit × PRECISION / total_shares + +// A depositor's earnings = their shares × how much rps has risen since they last touched the pool: +live_reward = pending + shares × (pool.reward_per_share − user.snapshot) / PRECISION +``` + +通俗地说:每位存款人「拥有」每次 `reward_per_share` 上升中的一份,其奖励 = +`份额 × (当前 rps − 上次存取时记录的 rps)`。存款人自己的记录**仅在其行动时**(存入/提取)才被触及;在此之前 +其权益在全局数字中静默累积。因此向成千上万存款人分配利润是 **O(1)**(一次加法),且在领取前不付出任何资金。 +这是出自 +[SushiSwap MasterChef 合约](https://github.com/sushiswap/masterchef/blob/master/contracts/MasterChef.sol) +(及 Compound 的 cToken 指数)的著名累加器模式;`PRECISION`(1e9)保持整数除法精确。 + +### 5.4 机会成本保护 + +池向每个市场自动分配,形成一个攻击面:恶意预言机可创建长期零成交量市场以锁定池资金。三种机制应对: + +**渐进式提前召回:** 市场时长分为 10 步。每步若下注量低于阈值(分配的 1%),则将当前分配的 10% 召回池中。 +完全闲置的 30 天市场会失去约 60% 的分配。 + +**活跃市场惩罚:** 同一预言机每多一个活跃市场,其分配减少 5%(递归)。拥有 10 个活跃市场的预言机每个市场仅获 +约 60% 的基础分配,激励质量而非数量。 + +**故障惩罚印记:** 不良结果(错过截止、败诉、零成交量裁定)产生惩罚印记,进一步降低未来分配。印记在 10 天干 +净运营后自动到期。 + +### 5.5 可选杠杆(由懒惰池提供资金) + +懒惰池**从一个 `free_balance` 承担两种角色**:静默的市场 LP 分配*以及*为**可选杠杆**子系统提供资金。下注者 +可开立杠杆持仓(`pm_leverage_open`),其保证金为**来自池的贷款**——不增发代币,从系统视角看持仓始终足额抵 +押。会击垮 CLOB 清算引擎的二元「跳空风险」通过**按下注前储备清算**处理:对向下注或结算强平回收 +`min(cancel_value, obligation) ≥ loan`,故池取回其贷款外加利息;唯一有界的坏账路径是同侧 +`pm_cancel_bet`。中位数 kill-switch(`pm_leverage_enabled`,默认关)阻止*新*开仓,但保护性清算级联刻意 +**不**受其门控——关闭杠杆永不剥夺已开持仓的保护。池在 LP 收益之外赚取杠杆利息,按相同的 MasterChef 方式记账。 +池贷款的结算发出 `pm_leverage_resolve` / `pm_leverage_liquidate`。 + +--- + +## 6. 治理 + +### 6.1 代表投票的链参数 + +VIZ 使用委托权益证明(DPoS)共识,由当选代表(验证人)通过中位数投票机制治理链参数: + +1. 每位代表公布所有参数的偏好值 +2. 网络计算所有活跃代表投票的**中位数** +3. 中位数移动时参数自动改变——无需硬分叉、无需部署 + +所有预测市场参数(费用、罚则、保险要求、争议窗口、懒惰池设置、杠杆旋钮、批量/提交-揭示时序)均由代表投票。所 +有百分比参数均以**基点(bp),10000 = 100.00%**: + +| 示例 | 治理 | +|----------|-----------| +| `pm_dispute_fee`、`pm_max_oracle_fee_percent`(bp) | 代表中位数投票 | +| `pm_dispute_grace_sec`、`pm_dispute_vote_period_sec` | 代表中位数投票 | +| `pm_dispute_approve_min_percent`、`pm_dispute_reward_multiplier`(bp) | 代表中位数投票 | +| `pm_lazy_*` 分配/召回、`pm_leverage_*`(enabled、fund %、max position) | 代表中位数投票 | +| `pm_commit_reveal_enabled`、`pm_batch_epoch_blocks`、`pm_reveal_window_blocks` | 代表中位数投票 | + +硬分叉仅用于结构性变更(新操作类型、公式改变),而非经济调参。Kill-switch(`pm_leverage_enabled`、 +`pm_commit_reveal_enabled`)让治理可通过中位数投票停用整个子系统而无需分叉。 + +### 6.2 司法辖区客户端模型 + +VIZ DLT 是基础设施,而非运营方——类似于比特币是账本而非货币传输方。协议中立且无需许可。法律义务附着于**客户 +端应用**,而非共识算法。 + +任何司法辖区都可在 VIZ DLT 上构建合规客户端: + +| 客户端组件 | 实现 | +|-----------------|---------------| +| 预先批准的预言机 | 客户端持有许可、经 KYC 验证的预言机白名单 | +| 预先批准的裁决者 | 政府批准的争议解决机构 | +| KYC/AML | 客户端层身份验证 | +| 费用路由为税收 | `dao_fund_account_id` → 国库账户 | +| 市场限制 | 客户端按允许类别过滤 | +| 下注限额 | 客户端强制的人均上限 | + +相同的协议操作(`pm_place_bet`、`pm_resolve`、`pm_dispute`)对无许可与受监管客户端运行一致。差异完全在客 +户端层。 + +--- + +## 7. 竞争格局 + +### 7.1 平台对比 + +| 维度 | Onix (Forecaster) | Polymarket | Kalshi | Standard LMSR | +|-----------|-------------------|------------|--------|---------------| +| **定价** | CPMM(二元)/ LMSR softmax(多元) | CLOB | CLOB | LMSR | +| **LP 风险** | **零**(结构性保障) | 库存风险 | 不适用 | 高达 `b × ln(N)` | +| **LP 知识门槛** | 低(存入即赚) | 高(管理订单) | 不适用 | 中 | +| **费用模型** | 裁定时输家池的 % | 买卖价差 | 交易所费用(1-7%) | 价差 | +| **预言机** | 每市场保证金 + 委员会争议 | UMA 乐观预言机 | Kalshi(CFTC 监管) | 运营方 | +| **迟下注惩罚** | 二次、可配置 | 无 | 无 | 无 | +| **持仓转让** | 原生协议操作 + 加密 memo | CTF(ERC-1155) | 无 | 无 | +| **治理** | 代表投票参数 | 团队多签 | CFTC 流程 | 运营方 | +| **基础设施** | VIZ DLT(共识级) | Polygon(智能合约) | 专有服务器 | 各异 | + +### 7.2 为何不需要 CTF Split/Merge + +Polymarket 使用 Gnosis Conditional Tokens Framework(CTF),其持仓为可拆分与合并的 ERC-1155 代币以强制价 +格一致性(价格之和 = $1)。 + +在 Onix 协议下,此机制在架构上不必要: + +- **Onix Binary(CPMM):** `price(A) + price(B) = reserve_b/(reserve_a+reserve_b) + reserve_a/(reserve_a+reserve_b) = 1` —— 按定义 +- **Onix Multi(LMSR softmax):** `Σ price(i) = Σ exp(q_i/b) / Σ exp(q_j/b) = 1` —— 按 softmax 定义 + +无需套利机制。价格一致性是公式的数学属性,而非外部强制层。 + +### 7.3 飞轮 + +``` +Risk-free LP → lower barrier for retail LPs + → more liquidity deposited + → deeper markets, less slippage + → better UX for bettors + → more volume + → more fees for LPs + → attracts even more LPs +``` + +「无无常损失的被动收益」是 Uniswap、Balancer 与 Curve 无法提供的价值主张。对加密原生受众而言,这是一个引人 +注目的叙事:通过为预测市场提供流动性赚取收益,本金零风险。 + +--- + +## 8. VIZ DLT:从原型到协议 + +### 8.1 当前状态 + +协议起步为带中心化后端的 Telegram WebApp(所有市场逻辑在服务端)——一个有已知限制的可用原型:除 Telegram +账户外无女巫抵抗、无抗审查、无可组合性。**该迁移现已完成:** 完整市场逻辑作为 **VIZ DLT 上经共识验证的 +`pm_*` 操作**运行(HF14),在 `consensus_sim` 中端到端验证。本节其余部分描述这一现已实现的链上架构。 + +### 8.2 迁移架构 + +VIZ DLT 是一种分布式账本技术,约 3 秒出块、DPoS 共识、命名账户(Graphene 风格),且无通用智能合约。预测市 +场操作实现为**一等公民、经共识验证的操作**——非智能合约,非 `custom_json` 负载。 + +| 层 | 示例 | 经共识验证? | +|-------|---------|---------------------| +| **协议操作** | `pm_create_market`、`pm_oracle_accept_market`、`pm_place_bet`、`pm_commit_bet`/`pm_reveal_bet`、`pm_resolve_market`、`pm_dispute_create`/`pm_dispute_vote`/`pm_dispute_resolve`、`pm_lazy_deposit`/`pm_lazy_withdraw`、`pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert` | 是——每个节点验证 | +| **虚拟操作** | `pm_payout`(每下注者)、`pm_auto_payout`、`pm_market_accepted`、`pm_dispute_finalize`、`pm_dispute_auto_close`、`pm_oracle_missed_penalty`、`pm_lazy_recall`、`pm_batch_settle`、`pm_commit_forfeit`、`pm_leverage_resolve`/`pm_leverage_liquidate` | 是——确定性,在出块时生成 | +| **metadata / custom_json** | 争议评论、市场描述、UI 元数据 | 否——仅展示/索引 | + +每个金融动作(下注、添加流动性、裁定市场、扣押保险)都由每个验证人验证。无效操作在入块前被拒。无 Solidity、 +无 gas 估算、无字节码部署。 + +前端是**完全无头的 Web 客户端**——无后端服务器、无数据库、无会话。私钥存于浏览器(加密),交易本地签名并广播 +到公共 VIZ 节点。不依赖 Telegram;核心应用与平台无关。 + +### 8.3 自原型以来已交付,及剩余路线图 + +**已链上交付(HF14):** + +| 特性 | 状态 | +|---------|--------| +| 提交-揭示 + 批量下注(二元、可选、中位数 kill-switch) | ✅ 上线 | +| 可选杠杆(懒惰池提供资金、下注前储备清算) | ✅ 上线 | +| 懒惰池(自动分配、渐进式召回、MasterChef 记账) | ✅ 上线 | +| 每下注者 / 杠杆结算虚拟操作 + 插件 API | ✅ 上线 | +| 懒惰池质押作为治理权重(PM 争议 + DAO 提案) | ✅ 上线 | + +**剩余路线图:** + +| 优先级 | 特性 | 影响 | +|----------|---------|--------| +| 高 | 共享流动性池(类别级 AMM) | 在架构层面解决流动性碎片化 | +| 高 | 自动数据预言机(外生数据源) | 消除客观市场的操纵 | +| 中 | 分层争议窗口(小市场 vs 大市场) | 更好的 UX 校准 | +| 中 | LMSR 批量结算(将批量/提交-揭示扩展到多元) | 多元市场目前强制即时下注 | +| — | **争议**的提交-揭示投票 | **刻意拒绝**——争议保持公开听证(见 §4.2) | + +--- + +## 9. Onix 不主张什么 + +对取舍与局限的诚实披露: + +- **LP 利润不被保证。** 若某市场零输家下注,则无手续费可分配。LP 取回本金但无收益。 + +- **存在平台风险。** 漏洞、攻击与治理攻击独立于做市商模型。LP 保障是结构性的(赔付架构),并非保险(无外部担保 + 基金)。 + +- **Onix Multi 代币并非固定面值工具。** 在标准 LMSR 中,1 个获胜代币 = 1 单位货币。在 Onix Multi 中,代币是 + 对输家池的比例索取权。若所有下注者都选中赢家,则人人保本。 + +- **LP 收益取决于成交量,而非深度。** 补贴 100,000 VIZ 的市场与补贴 1,000 VIZ 的市场,若成交量与费率相同, + 赚取相同的绝对手续费。补贴提供深度,而非收益。 + +- **DPoS 治理有已知取舍。** 验证人少于 PoW/PoS、代表集中风险、按代币加权投票。这些是 DPoS 模型固有的(EOS、 + Hive、Tron 共有),非 VIZ 特有。 + +- **VIZ 代币流动性目前较低。** 经济保障(保险保证金、争议费)随代币价格扩展。协议假设效用随时间驱动需求——这 + 是每个协议原生代币项目所下的同一注。 + +--- + +## 10. 结论 + +Onix 协议针对预测市场采用的根本障碍:LP 风险。通过在二元(CPMM)与多元(LMSR + 同注分彩)市场中结构性地将 +LP 资金与下注结算分离,Onix 使流动性提供变为无风险且对零售参与者可及。 + +关键创新: + +1. **LP 本金保障** 作为架构不变量,而非保险 +2. **输家资助赢家** 结算,消除手续费与赢家赔付的竞争 +3. **LMSR 定价 + 同注分彩结算**(Onix Multi)——将经验证的价格发现与 LP 安全结合 +4. **时间加权 LP 分配**,奖励早期资本投入 +5. **二次时间惩罚** 作用于利润(永不本金),用于迟下注 +6. **懒惰流动性池**,具自动分配、渐进式召回与 MasterChef 记账——同时为可选**杠杆**子系统提供资金(池提供保 + 证金、下注前储备清算、除有界 cancel-bet 路径外无坏账) +7. **保证金预言机模型**,具声誉评分、offer→quote 费用冻结与双模式争议系统——委员会争议为**公开听证**,投票 + 可修改且按懒惰池加权 +8. **可选抗 MEV**——批量 / 提交-揭示下注(二元),带中位数 kill-switch +9. **共识级实现** 于 VIZ DLT——无智能合约、无 gas、无外部 keeper;严格**零和**(协议从不增发代币) + +赌注很简单:若无风险 LP 吸引资本、资本创造深度、深度改善价格、价格吸引下注者,那么 Onix 协议便解决了预测市场 +的流动性问题。协议机制可数学验证。经济假设将由市场检验。 + +--- + +## 11. 作者与披露 + +### 作者 + +**Anatoly Piskunov**(On1x)—— 俄罗斯 IT 创新者、Web3/DLT 开发者、VIZ 区块链的创建者。其工作横跨分布式账 +本技术、去中心化社交协议与数字社区的经济模型。 + +主要贡献包括:VIZ 区块链(Fair DPoS、社会资本原语)、Onix 协议(具 LP 保障的预测市场)、Voice 协议(抗审查 +消息)以及关于区块链经济学与 Web3 架构的大量出版物。 + +完整出版物与项目列表:[https://on1x.com](https://on1x.com) + +### 披露 + +Forecaster 与 Onix 协议的作者同时是 VIZ DLT 的创建者。迁移路线图提议将平台迁至作者设计并构建的区块链。 + +这一点已预先披露。这也是常态:Polymarket 依赖 Polygon Labs 的基础设施,Kalshi 运行在自有服务器上,Augur +设计了其运行所依赖的 REP 代币。每个平台都为自身基础设施辩护。问题不在于作者是否有利益——他们总有——而在于技 +术主张是否可证伪。本文中的每一条公式、证明与机制都可数学验证,且代码库开源。 + +--- + +## 12. 参考文献 + +1. Hanson, R. (2003). *Combinatorial Information Market Design.* Information Systems Frontiers, 5(1), 107–119. —— 对数市场评分规则(LMSR)。 + +2. Adams, H., Zinsmeister, N., Robinson, D. (2020). *Uniswap v2 Core.* —— 恒定乘积做市商(`x * y = k`)。 + +3. Adams, H., et al. (2021). *Uniswap v3 Core.* —— 集中流动性与无常损失分析。 + +4. Gnosis. *Conditional Tokens Framework (CTF) Documentation.* https://docs.gnosis.io/conditionaltokens/ —— ERC-1155 预测市场持仓。 + +5. UMA Protocol. *Optimistic Oracle Documentation.* —— Polymarket 使用的争议升级机制。 + +6. Leshner, R., Hayes, G. (2019). *Compound: The Money Market Protocol.* —— cToken 累加器模式(reward_per_share 的基础)。 + +7. SushiSwap. *MasterChef Contract.* —— 奖励分配的懒惰记账模式。 + +8. Piskunov, A. (2019). *VIZ blockchain system: technical description.* —— VIZ DLT 架构、DPoS 共识、命名账户。 + +9. Piskunov, A. (2019). *What is Fair DPoS.* —— 委托权益证明中的治理创新。 + +10. Piskunov, A. (2023). *VIZ as a Digital Representative Self-Governing State.* —— 区块链系统作为数字政体的框架。 diff --git a/@l10n/zh-CN/docs/prediction-markets/workflows.md b/@l10n/zh-CN/docs/prediction-markets/workflows.md new file mode 100644 index 0000000000..25119631ce --- /dev/null +++ b/@l10n/zh-CN/docs/prediction-markets/workflows.md @@ -0,0 +1,687 @@ +--- +title: 预测市场 —— 工作流与交互图 +description: 一个典型的 Onix 二元市场贯穿每个参与者,附正常与争议裁定的零和主账本。 +--- + +# 工作流与交互图 + +一个**典型场景**贯穿每个参与者。每个角色发送特定的**已签名操作**,被特定的**虚拟操作**触及,并以两种结果的 +**代币 发送 / 接收** 表收尾: + +- **正常裁定** —— 预言机裁定,宽限期过去,`pm_auto_payout` 结算。无争议。 +- **争议裁定** —— 预言机裁定 **A**,争议**翻转为 B**,随后进行结算。 + +所有金额为抽象 **VIZ**。所有百分比为 **bp**(10000 = 100.00%)。结算严格**零和**——从不增发代币, +`current_supply` 不变: + +``` +Σ winner_payout + oracle_take + creator_take + lp_bonus + LP_principal + == Σ all bet amounts + LP_principal + forfeit_pool (+ insurance slash, 争议中) +``` + +## 典型市场 **M**(binary CPMM,A vs B) + +| 项 | 值 | +|------|-------| +| 引擎 | binary CPMM(`x·y=k`),`weight = tokens_out` | +| 种子流动性(marketmaker) | **2000** → 储备 A=1000 / B=1000 | +| `oracle_fee_percent`(预言机报价) | **1000**(10%) | +| `creator_fee_percent` | **500**(5%) | +| `liquidity_fee_percent` | **500**(5%) | +| `oracle_fixed_fee`(预言机报价) | **10** | +| `dispute_penalty_percent` | **+10000**(罚没至多 100% 保险 ×consensus) | + +示例链参数:`pm_market_creation_fee` 5、`pm_oracle_registration_fee` 10、`pm_min_oracle_insurance` 5000、 +`pm_dispute_fee` 1000、`pm_dispute_reward_multiplier` 30000(**3×**)、`pm_no_contest_penalty_percent` +5000、`pm_oracle_penalty_percent` 500、`pm_lazy_emergency_penalty_percent` 5000、 +`pm_leverage_pool_profit_percent` **R = 10%**、`pm_lazy_alloc_percent` 2000(20%)。 + +### 角色表 + +| 角色 | 职责 | 质押 / 动作 | +|-------|------|----------------| +| **maker** | 创建者 + 首位 LP | 种子 2000 流动性 | +| **orac** | 外部预言机 | 保险 5000;报价 10% + 固定 10 | +| **LP1** | 市场内流动性提供者 | 增加 1000 | +| **A** | 下注者 —— 赢家 | 100 于 **A**,早期;权重 100 | +| **C** | 下注者 —— 迟到赢家 | T+85% 时 100 于 **A**;权重 100;时间惩罚 **50%** | +| **B** | 下注者 —— 输家 | 200 于 **B**;权重 200 | +| **D** | 杠杆 **×10** 赢家 | collateral 10 + loan 90(市场 **L**) | +| **E** | 杠杆 **×5** 被清算 | collateral 20 + loan 80(市场 **L**) | +| **LZ1** | 懒惰池存款人 | 存入 1000 | +| **disp** | 争议提出者 | 托管争议费 1000 | + +> 曲线权重(100 / 100 / 200)显式写出以便同注分彩的算术可读;真实 CPMM 随储备移动会给出略少的权重。 + +## 交互图 + +**市场生命周期。** + +```mermaid +flowchart LR + W["Waiting (0)"] -->|预言机接受| A["Active (1)"] + W -->|预言机拒绝| X["Deleted (-1)"] + A -->|betting_expiration| C["Closed (2)"] + A -->|提前裁定| R["Resolved (3)"] + C -->|预言机裁定| R + R -->|宽限期,无争议| P["已赔付"] + R -->|提交争议| D["Disputed"] + D -->|finalize / 裁决者| P +``` + +**结算 —— 正常裁定(A 获胜)。** 输家资助赢家;LP 本金不变(零和)。 + +```mermaid +flowchart TD + B["B 输 200(输家池)"] --> POOL{"拆分 200"} + POOL -->|oracle_fee 20 + fixed 10| OR["预言机 +30"] + POOL -->|creator_fee 10| CR["创建者 +10"] + POOL -->|liq_fee 10 + 惩罚 37| LPS["LP +47"] + POOL -->|winners_pool 150 → 利润 75| A["A → 赔付 175"] + POOL -->|利润 75 − 时间惩罚 37| C["C → 赔付 138"] + MK["maker + LP1 本金 3000"] -.全额返还.-> MK +``` + +**争议 —— 预言机裁 A,翻转为 B。** 惩罚是保险罚没(独立资金)。 + +```mermaid +sequenceDiagram + participant O as 预言机 + participant D as 提出者 + participant V as 委员会 / 裁决者 + O->>O: 裁定 A + D->>V: pm_dispute_create(托管 dispute_fee) + O-->>V: 强制响应(截止) + V->>V: pm_dispute_vote / pm_dispute_resolve → 翻转为 B + V-->>O: 保险罚没(5000) + V-->>D: 退费 + 奖励(从罚没中 2000) + V->>V: 重新结算 → B 获胜 +``` + +## 主账本 —— 正常裁定(A 获胜) + +`losers_sum = 200`(B)。从输家池扣费: +`oracle_fee = 200×10% = 20`、`creator_fee = 200×5% = 10`、`liq_fee = 200×5% = 10`、`oracle_fixed = 10`。 +`winners_pool = 200 − 20 − 10 − 10 − 10 = 150`。`Σ 获胜权重 = 200`(A 100 + C 100)。 + +- **A**:利润 `150×100/200 = 75`,惩罚 0 → **赔付 175**。 +- **C**:利润 75,时间惩罚 `75×50% = 37`(→ LP)→ **赔付 138**。 +- **LP 奖励** = `liq_fee 10 + 惩罚 37 = 47`,按在市场中的时间拆分:**maker ~31 / LP1 ~16**。 +- **oracle_take** = `oracle_fee 20 + fixed 10 = 30`。**creator_take** = `creator_fee 10`。 + +| 角色 | 发送 | 接收 | 净额(本市场) | +|-------|-------|----------|-------------------| +| maker | 2000 流动性 + 5 创建费 | 2000 本金 + 10 创建者费 + 31 LP 奖励 | **+36** | +| orac | (10 注册费,5000 保险锁定) | 30 oracle-take | **+30** | +| LP1 | 1000 流动性 | 1000 本金 + 16 LP 奖励 | **+16** | +| A | 100 | 175 | **+75** | +| C | 100 | 138 | **+38** | +| B | 200 | 0 | **−200** | + +**零和:** in `= 下注 400 + LP 本金 3000 = 3400`;out `= 175+138+0 + 30 + 10 + 47 + 3000 = 3400`。✔ +5 创建费 + 10 注册费进入 **DAO 基金**(不属于市场池)。 + +## 主账本 —— 争议裁定(预言机裁 A → 翻转为 B) + +`disp` 托管 `dispute_fee 1000`。裁决翻转为 **B**;预言机被罚没。 +当 `dispute_penalty_percent = 10000` 且共识强度 **100%**:`slash = 5000×100%×100% = 5000`。 +奖励切分:`reward_target = fee×3 = 3000` → `bonus = 3000 − 1000 = 2000`(≤ slash)。提出者获得 +`fee 1000 + bonus 2000 = 3000`。余额 `slash − bonus = 3000 → forfeit_pool`。 + +现在 **B 获胜**。`losers_sum = 200`(A 100 + C 100)。费用 20/10/10 + fixed 10。 +`winners_pool = 200 − 50 + forfeit 3000 = 3150`。`Σ 获胜权重 = 200`(B)。 +- **B**:利润 `3150×200/200 = 3150` → **赔付 3350**。 +- **oracle_take** 仍为 `30`(即使翻转,*市场*费用仍按冻结配置支付——惩罚是**保险罚没**,独立资金)。 + **creator_take** 10。**LP 奖励** = liq 10。 + +| 角色 | 发送 | 接收 | 净额(本市场) | +|-------|-------|----------|-------------------| +| maker | 2000 + 5 | 2000 本金 + 10 创建者费 + ~6 LP 奖励 | **+11** | +| orac | 保险 −**5000** 罚没 | 30 oracle-take | **−4970** | +| LP1 | 1000 | 1000 本金 + ~4 LP 奖励 | **+4** | +| A | 100 | 0 | **−100** | +| C | 100 | 0 | **−100** | +| B | 200 | 3350 | **+3150** | +| disp | 1000 争议费 | 3000(退费 + 2000 奖励) | **+2000** | + +**零和:** in `= 下注 400 + LP 本金 3000 + dispute_fee 1000 + 罚没 5000 = 9400`; +out `= B 3350 + 预言机 30 + 创建者 10 + lp_bonus 10 + LP 本金 3000 + 提出者 3000 = 9400`。✔ +罚没 5000 拆为提出者奖励 2000 + forfeit 3000(→ 经赢家池归 B)。 + +## 实现状态(已与代码核对) + +**常规操作 —— 全部 21 个**存在于 `operation` variant(`operations.hpp`),在 `pm_evaluator.cpp` 校验 + 求值: +`pm_oracle_register`、`pm_oracle_update`、`pm_create_market`、`pm_oracle_accept_market`、`pm_place_bet`、 +`pm_commit_bet`、`pm_reveal_bet`、`pm_cancel_bet`、`pm_add_liquidity`、`pm_withdraw_liquidity`、 +`pm_resolve_market`、`pm_no_contest`、`pm_dispute_create`、`pm_dispute_vote`、`pm_dispute_resolve`、 +`pm_transfer_position`、`pm_lazy_deposit`、`pm_lazy_withdraw`、`pm_leverage_open`、`pm_leverage_close`、 +`pm_leverage_convert`。✔ + +**虚拟操作** —— 由 `database::process_pm_markets()` / 求值器发出: + +| 虚拟操作 | 触发? | 触发点(代码) | +|------------|--------|----------------| +| `pm_market_accepted` | ✔ | `pm_oracle_accept_market` **以及**自预言机 `pm_create_market` | +| `pm_payout` | ✔ | 结算时**每笔活跃下注**——携带 `account`、`market_id`、`bet_id`、`side`/`outcome_index`、`amount`(本金)、`payout`(**输则 0**) | +| `pm_auto_payout` | ✔ | 结算时**每个市场一次**——汇总标记(`bets_sum`),与逐笔 `pm_payout` 并列 | +| `pm_commit_forfeit` | ✔ | 超过 `reveal_deadline` 未揭示的承诺 | +| `pm_dispute_finalize` | ✔ | 委员会 `voting_end_time` | +| `pm_dispute_auto_close` | ✔ | `auto_close_time`(防冻结) | +| `pm_oracle_missed_penalty` | ✔ | 预言机错过 `result_expiration` | +| `pm_lazy_recall` | ✔ | 闲置分配的渐进式召回步 | +| `pm_batch_settle` | ✔ | 纪元边界 | +| `pm_leverage_liquidate` | ✔ | 盘中清算:reason **0** 对向下注、**1** 撤注(`cascade_liquidate`) | +| `pm_leverage_resolve` | ✔ | 杠杆头寸的**结算**:携带 `market_id`、`outcome_index`、`won`、`pool_received`/`bettor_received`、`leverage`(= `total_bet/collateral`) | + +读取方法见[插件 API](../plugins/prediction-market-api) +(`get_account_leverage_positions`、`get_market_leverage_positions`、`get_creator_ban`、`get_dispute_votes`、…); +逐下注者结果(`pm_payout`)与杠杆结算(`pm_leverage_resolve`)亦见于 `account_history`。 + +## 典型场景中的角色 + +每个参与者都被贯穿市场 **M**(及杠杆子市场 **L**)来追踪:其交互图、它发送的**签名**操作、触及它的**虚拟** +操作、两种结果的**发送 / 接收**账本,以及代码核对指引。每个按角色的账本都是上述两份主账本的切片。 + +### 做市商(创建者 + 首个 LP) + +做市商创建市场 M,注入 **2000** 流动性(成为首个 `pm_liquidity_object`),并提出预言机的**报价上限**。它 +**不**裁定(那是预言机的事)。 + +```mermaid +flowchart LR + maker -->|pm_create_market| M[(pm_market_object
status=0)] + maker -->|seed 2000| LP0[(pm_liquidity_object
provider=maker)] + M -. fee 5 .-> DAO[(committee_fund)] + orac -->|pm_oracle_accept_market| M2[(M status=1)] + M2 -. VIRTUAL .-> VA[[pm_market_accepted]] + M2 ==>|pm_auto_payout| RET[principal 2000 + creator_fee + LP bonus] + RET --> maker +``` + +- **发送:** `pm_create_market`(将 `oracle_fee_percent`/`oracle_fixed_fee` 设为**报价上限**,加上自身的 + `creator_fee_percent` 5% 与 `liquidity_fee_percent` 5%;支付 `pm_market_creation_fee` 5 → DAO,锁定 + `liquidity` 2000);可选 `pm_add_liquidity` / `pm_withdraw_liquidity`(本金安全,从 `betting_expiration` + 到裁定锁定)。 +- **被触及:** `pm_market_accepted`(预言机接受,或自预言机在创建时);`pm_auto_payout`(返还 LP 本金 + + 按时间加权的 LP 奖励份额)。 + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 2000 流动性 + 5 创建费(→DAO) | 2000 本金 + **creator_fee 10** + **LP 奖励 ~31** | **+36** | +| 争议(→B) | 2000 + 5 | 2000 本金 + creator_fee 10 + LP 奖励 ~6 | **+11** | + +翻转时创建费仍从冻结的市场配置**照付**——争议惩罚的是**预言机**(保证金罚没),而非做市商。LP 本金无条件返还。 + +- **自预言机:** `oracle == creator` → 创建即生效,`pm_market_accepted` 带 `self_oracle=true`,做市商另外 + 赚取 `oracle_take`。 +- **核对:** `pm_create_market_evaluator`;LP 经 `settle_liquidity`;`committee_fund += pm_market_creation_fee`。 + **观察:** `get_market`、`list_markets_by_creator`、`get_market_liquidity`(`earned_fee`)、`get_market_meta`。 + +### 预言机(注册 → 接受-报价 → 裁定) + +外部预言机 **orac** 质押保证金,在接受时**报价**其费用(≤ 做市商报价且 ≤ `pm_max_oracle_fee_percent`)并裁定。 +其市场费从输家池支付;保证金仅在错过截止或争议败诉时承压。 + +```mermaid +flowchart LR + orac -->|pm_oracle_register
insurance 5000| O[(pm_oracle_object)] + orac -. reg-fee 10 .-> DAO[(committee_fund)] + orac -->|pm_oracle_accept_market
quote fee 10% + fixed 10| M[(M status=1)] + M -. VIRTUAL .-> VA[[pm_market_accepted]] + orac -->|pm_resolve_market A| M3[(M status=3)] + M3 ==>|grace passes| VP[[pm_auto_payout]] + VP -->|oracle_take 30| orac +``` + +- **发送:** `pm_oracle_register`(锁定保证金 5000,支付 reg-fee 10 → DAO,设定建议价目表); + `pm_oracle_accept_market`(**报价** fee 10% + fixed 10,各 ≤ 创建者报价且 ≤ 中位上限;冻结到 M); + `pm_resolve_market`(设 `winning_outcome`,开启宽限);可选 `pm_oracle_update` / `pm_no_contest`。常驻价目表 + 亦可在创建时自动接受市场使其上线——见预言机操作文档。 +- **被触及:** `pm_market_accepted`;`pm_auto_payout`(计入 `oracle_take`);`pm_oracle_missed_penalty` + (从未裁定 → 罚没 `pm_oracle_penalty_percent` 保证金 → DAO,全额退还下注)。 + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 保证金 5000(锁定)+ reg-fee 10(→DAO) | **oracle_take 30** = fee 20 + fixed 10 | **+30** | +| 争议(→B) | 保证金 −**5000 罚没** | oracle_take 30 | **−4970** | + +即便翻转,预言机仍保留小额**市场费**(冻结配置);惩罚是**保证金罚没**,拆分为争议者奖励与赢家 `forfeit_pool`。 +报价**低于**报价允许(价格=声誉);**高于**则拒绝。 + +- **核对:** `pm_oracle_register_evaluator`、`pm_oracle_accept_market_evaluator`(≤ 报价、≤ 上限、冻结)、 + `process_pm_markets` 错过截止扫描。**观察:** `get_oracle`、`list_oracles`、`get_market`(冻结条款)。 + +### 预言机 —— 争议中被维持(争议胜方) + +预言机裁定 **A**;争议者挑战但裁决**维持** A。预言机保留市场费**并**收取被没收的 `dispute_fee`;保证金不动, +`disputes_won++`。 + +```mermaid +flowchart LR + orac -->|pm_resolve_market A| M[(market resolved A)] + disp -->|pm_dispute_create| D[(dispute)] + D ==>|uphold A| FIN[[pm_dispute_finalize / resolve]] + FIN -->|dispute_fee 1000| orac + FIN --> AUTO[[pm_auto_payout settles A]] + AUTO -->|oracle_take 30| orac +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 争议,维持 | 保证金 5000(**不**罚没) | oracle_take 30 + **dispute_fee 1000** | **+1030** | +| 正常(无争议) | 保证金 5000(锁定) | oracle_take 30 | **+30** | + +挑战反噬争议者并**支付给预言机**。善意市场(`dispute_penalty_percent < 0`)甚至能在改变结果时给预言机费用 +奖励——承认诚实错误。**核对:** `pm_dispute_finalize` / `pm_dispute_resolve` 的 uphold 分支。**观察:** +`get_oracle`(`disputes_won`)、`get_dispute`。 + +### 预言机 —— 被翻转 + 罚没(争议败方) + +预言机裁定 **A**;争议**翻转为 B**,保证金被**罚没**。它仍收取微小的冻结市场费(费用与惩罚是两笔钱),但损失 +大部分保证金与声誉。 + +```mermaid +flowchart LR + orac -->|pm_resolve_market A| M[(resolved A)] + disp -->|pm_dispute_create proposed=B| D[(dispute)] + D ==>|overturn to B| FIN[[pm_dispute_finalize / resolve]] + FIN -->|slash 5000| INS[oracle.insurance ↓] + INS --> SPLIT[bonus 2000 → disputer
3000 → forfeit_pool → B] + FIN --> AUTO[[pm_auto_payout settles B]] + AUTO -->|oracle_take 30| orac +``` + +`slash = 保证金 5000 × dispute_penalty_percent (100%) × consensus_strength (100%) = 5000`,是**再分配**而非 +销毁:`bonus 2000 →` 争议者,`3000 → forfeit_pool →` 新赢家(B)。净 **−4970** 对比无争议的 **+30**。罚没随 +**共识强度**(`winning_rshares / max_rshares`)缩放;`dispute_penalty_percent < 0`(善意)→ **无**罚没。被 +罚没的预言机往往也被**封禁**(下一角色)。**核对:** `pm_dispute_finalize` / `pm_dispute_resolve` 的 overturn +分支;费用仍取自 `mkt.oracle_fee_percent`。**观察:** `get_oracle`(`total_insurance_slashed`、`banned_until`)、 +`get_dispute`。 + +### 被封禁的预言机(及被封禁的创建者) + +封禁是**状态**,非转账:`pm_oracle_object.banned_until`(创建者则是 `pm_creator_ban_object`)阻止该角色承接 +**新**市场直到时间戳过去。它通常伴随翻转罚没,但本身不移动代币。 + +```mermaid +flowchart LR + resolver -->|pm_dispute_resolve ban_oracle| O[(pm_oracle_object
banned_until = T)] + orac -->|pm_create_market / accept| CHK{now < banned_until?} + CHK -->|yes| REJ[REJECTED: 'Oracle is banned'] + CHK -->|no, expired| OK[allowed again] + resolver -->|ban_creator| CB[(pm_creator_ban_object)] + maker -->|pm_create_market| CHK2{banned?} + CHK2 -->|yes| REJ2[REJECTED: 'Creator is banned'] +``` + +- **谁设定:** 账户模式 → `pm_dispute_resolve`(`ban_oracle`/`ban_creator` + `…_until`);委员会模式 → + `pm_dispute_finalize` 在翻转时按共识缩放封禁。`banned_until = time_point_sec::maximum()` ⇒ **永久**。 +- **代币:** 封禁本身为 **0**(纯状态);伴随的罚没是上面的翻转情形。保证金仍锁定,封禁解除且无活跃市场后可退。 +- 封禁在快照中存续并按账户为键——重新注册无法抹除。**核对:** `pm_create_market_evaluator`(`"Oracle is + banned"` / `"Creator is banned"`)。**观察:** `get_oracle`(`banned_until`、`bans_received`)、 + **`get_creator_ban(account)`**。 + +### 下注者 A —— 早期赢家 + +**A** **早期在 A 侧下注 100**(无时间惩罚),M 裁定为 A 时获胜。赔付 = 本金 + 按权重比例的赢家池份额。 + +```mermaid +flowchart LR + A -->|pm_place_bet side=A 100| BET[(pm_bet_object
weight 100)] + BET --> M[(market M
reserves shift)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|payout 175| A +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 100 | **175** | **+75** | +| 争议(→B) | 100 | **0** | **−100** | + +`profit = winners_pool 150 × weight 100 / Σweight 200 = 75`;无惩罚 → 赔付 `100 + 75`。翻转使 A 成为**输**侧。 +赢利**仅**来自输家下注(+ forfeit 池),绝不来自增发。**发送:** `pm_place_bet`(`side=0`,instant);可选 +`pm_transfer_position` / `pm_cancel_bet`。**核对:** `pm_place_bet_evaluator`、`settle_market`。**观察:** +`get_account_positions`(`expected_payout`)、`get_market_weight_sums`;已实现的 `pm_payout` 见 `account_history`。 + +### 下注者 B —— 输家 + +**B** **在 B 侧下注 200**。M 裁定为 **A** 时,B 的下注资助赢家,B 一无所获。在争议路径中 B 成为赢家。 + +```mermaid +flowchart LR + B -->|pm_place_bet side=B 200| BET[(pm_bet_object
status active)] + BET --> M[(market M)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|status=resolved, payout 0| BET +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 200 | **0** | **−200** | +| 争议(→B) | 200 | **3350** | **+3150** | + +B 的 200 **就是** `losers_sum`(支付 40 费用 + 150 赢家池 + LP 奖励)——同注分彩的「输家资助赢家」规则。翻转时 +B 获胜,预言机 forfeit 3000 注入 B 池(`payout = 200 + 3150`)。输的下注仍由 `pm_payout` 以 **payout=0** 记录。 +**核对:** `settle_market` 输家分支。**观察:** `get_account_positions`、`get_market_bets`、`get_dispute`。 + +### 下注者 C —— 迟到赢家(时间惩罚) + +**C** **在 A 侧下注 100** 但**迟**(下注窗口 T+85%),故**时间惩罚**仅扣其*利润*(非本金)。权重与 A 相同但 +所得更少;被扣部分流向 LP。 + +```mermaid +flowchart LR + C -->|pm_place_bet side=A 100
at T+85%| BET[(pm_bet_object
weight 100
time_penalty 50%)] + BET --> M[(market M)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|payout 138| C + VP -. penalty 37 .-> LPb[LP bonus] +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 100 | **138** | **+38** | +| 争议(→B) | 100 | **0** | **−100** | + +`profit = 75`;`penalty = 75 × 50% = 37`(→ LP);`payout = 100 + 75 − 37 = 138`——同权重下比 A 的 +75 少 +**−37**。节点在下注时按市场惩罚曲线(`time_penalty_type/value`、`penalty_curve_type`)盖上 `time_penalty`。它 +遏制最后一秒抢狙并补贴流动性,而非协议。**核对:** `pm_place_bet_evaluator`(曲线求值)、`compute_settlement`。 +**观察:** `get_account_positions`(`time_penalty`)、`get_market_bets`。 + +### 下注者 D —— 杠杆 ×10 赢家 + +**D** 开 **×10** 仓:**10 抵押 + 90 贷款**(来自懒惰池)= 在 A 侧 **100**,于隔离杠杆市场 **L** +(`pm_leverage_enabled=true`,`R = 10%`)。A 获胜时,D 在偿还贷款 + 利息后保留全部 100 的上行。 +`pool_profit = loan 90 × R 10% = 9`;`obligation = 90 × 1.10 = 99`。 + +```mermaid +flowchart LR + D -->|pm_leverage_open
collateral 10 + loan 90| POS[(pm_leverage_position
total_bet 100, obligation 99)] + POOL[(lazy pool)] -.loan 90.-> POS + POS --> L[(market L, side A)] + L ==>|settle: force_close at cancel_value| VR[[pm_leverage_resolve won=true, leverage=10]] + VR -->|min(cv,obligation) 99| POOL + VR -->|cv 200 − 99 = 101| D +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 抵押 **10** | cancel_value 200 − obligation 99 = **101** | **+91** | +| 争议(→B) | 抵押 10 | 0 | **−10** | + +盈利仓按其 `cancel_value` 平仓;池收回 `obligation 99`(贷款 90 + **9 利息**),D 在自有 10 上保留余下 → +**+91**(池 **+9**)。杠杆按清算结算,**绝不**经 `pm_auto_payout`。零和(L):in `10 + 90 + 100 = 200`;out +`101 + 99 = 200`。**发送:** `pm_leverage_open`;可选 `pm_leverage_close`(仅当 `cv ≥ obligation`)/ +`pm_leverage_convert`。**被触及:** `pm_leverage_resolve`(结算强制平仓,`reason=expiration`)。**核对:** +`force_close_positions` → `liquidate_position(reason=2)`。**观察:** **`get_account_leverage_positions`** / +**`get_market_leverage_positions`**、`get_lazy_pool`。 + +### 下注者 E —— 杠杆 ×5 被清算 + +**E** 开 **×5** 仓:**20 抵押 + 80 贷款** = 在 B 侧 **100**(市场 **L**,`R = 10%`)。裁定前一笔**对向下注**将 +曲线推向不利于 B;**级联清算**强制平仓。**E 失去抵押,但池始终被补足。** `obligation = 80 × 1.10 = 88`。 + +```mermaid +flowchart LR + E -->|pm_leverage_open
collateral 20 + loan 80| POS[(pm_leverage_position
obligation 88)] + POOL[(lazy pool)] -.loan 80.-> POS + X -->|pm_place_bet side=A| L[(market L)] + L ==>|cascade at PRE-bet reserves
cv 88 ≤ threshold| VL[[pm_leverage_liquidate
reason=opposing_bet]] + VL -->|pool_received 88 = loan 80 + profit 8| POOL + VL -->|bettor_received 0| E +``` + +E 在裁定**之前**被清算,故最终 A/B 结果(是否争议)不改变它: + +| 角色 | 发送 | 接收 | 净额 | +|------|------|------|------| +| **E** | 抵押 **20** | **0** | **−20** | +| **池** | 贷款 80 | **88**(贷款 80 + R% 8) | **+8** | + +对向下注清算在**下注前**储备执行,此时 `cancel_value ≥ loan`,故 `pool_received = min(cv, obligation)` 至少返 +还贷款——池**绝不**亏损。 + +> **唯一可能为负的路径**是同侧 **`pm_cancel_bet`(Case B)**:取消会撤回一笔*先前的大额*同侧下注(超过单笔滑点 +> 上限),为对取消者公平,它**先**按其提交价执行——故级联可能落到 `cancel_value < loan`: +> `shortfall = obligation − cancel_value`,`lazy_pool.free_balance −= shortfall`。此**坏账**是**有界的** +> (`≤ cancel_value_before × SL%`)且**罕见**(池在其余所有仓位上的 R% 足以抵消)。由 +> `leverage_cancel_bet_cascade_bad_debt` 测试覆盖。 + +池保护是结构性的(`max_per_position`、`max_position_ratio`、`safety_margin`、滑点上限、`expiration_buffer`)。 +`pm_leverage_enabled=false` **仅**阻止**新**开仓——清算级联**不**受该标志门控,故治理永远无法在途中剥夺池的保护 +(`leverage_disabled_keeps_liquidation_protection`)。**核对:** `pm_place_bet` → `cascade_liquidate(reason=0)`; +`pm_cancel_bet` → `cascade_liquidate(reason=1)`;`liquidate_position`。**观察:** +**`get_account_leverage_positions`**(`status=1`、`pool_received`、`bettor_received`)、`get_lazy_pool`。 + +### 市场内流动性提供者(LP1) + +**LP1** 向活跃 M 添加 **1000** 流动性(在做市商种子之后)。本金**始终**返还;之上赚取**按时间加权**的 LP 奖励 +份额(流动性费 + 时间惩罚 + 零头)。与懒惰池提供者不同,后者一次存入并被自动分配到多个市场。 + +```mermaid +flowchart LR + LP1 -->|pm_add_liquidity 1000| L1[(pm_liquidity_object
provider=LP1)] + L1 --> M[(market M reserves)] + M ==>|settle| SL[[settle_liquidity]] + SL -->|principal 1000 + bonus ~16| LP1 + LP1 -->|pm_withdraw_liquidity
after resolution| OUT[principal-safe exit] +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 正常(A) | 1000 | **1000 本金 + ~16 奖励** | **+16** | +| 争议(→B) | 1000 | 1000 本金 + ~4 奖励 | **+4** | + +LP 奖励池 = `liq_fee 10 + 时间惩罚 37 = 47`,按 `本金 × 在场秒数` 分配(早期做市商 ~31,较晚的 LP1 ~16)。 +**本金保证**是架构性的——种子在任何赢家获付之前返还;LP 只会放弃奖励,绝不损失本金。提取从 +`betting_expiration` 到裁定锁定。**核对:** `pm_add_liquidity_evaluator`(记录 `deposit_time`)、 +`settle_liquidity` → `distribute_lp`。**观察:** `get_market_liquidity`(`earned_fee`)、`get_market_weight_sums`。 + +### 懒惰流动性池(系统对象) + +**单例** `pm_lazy_pool_object`——不是账户。存款人一次性注资;池**自动分配**一片给每个已接受市场作为静默 LP +(`pm_liquidity_object`,`provider` 为空)、**资助杠杆贷款**并**回收**闲置分配。它赚取 LP 收益 + 杠杆利息,按 +MasterChef 方式记账(一个全局 `reward_per_share`,O(1)——见白皮书)。字段:`total_shares`、`free_balance`、 +`allocated_balance`、`earned_balance`、`reward_per_share`、`leverage_fund_used`。 + +```mermaid +flowchart TD + LZ1 -->|pm_lazy_deposit 1000| POOL[(pm_lazy_pool
free 1000 / shares 1000)] + POOL ==>|on market accept
alloc 20% = 200| ALLOC[(pm_lazy_allocation
+ pm_liquidity provider=∅)] + ALLOC -->|market settles| YLD[route_pool_lp_return
principal 200 + yield 20] + YLD --> POOL + POOL -->|leverage loan 90| Dpos[D position] + Dpos -->|close/resolve: 90 + interest 9| POOL + POOL -. idle market .-> VR[[pm_lazy_recall]] + VR -->|step back to free| POOL +``` + +| 池资金流 | 效果 | +|----------|------| +| `pm_lazy_deposit` | `free_balance += amount`,铸造 shares | +| 自动分配(接受时) | `free → allocated`(静默 LP) | +| 市场结算 | `route_pool_lp_return`:本金 + 收益 → `free`;收益 → `earned` 与 `reward_per_share` | +| 杠杆开仓(D/E) | `free −= loan`,`leverage_fund_used += loan` | +| 杠杆平仓 / 结算 / 对向下注清算 | `min(cv, obligation) → free`;`cv ≥ loan` ⇒ **绝不亏损** | +| cancel-bet 清算(仅 Case B) | 回收 `cv`,**可能 < loan** → 有界**坏账** | +| `pm_lazy_recall`(闲置市场) | 闲置分配的一个 10% 步 → `free` | +| `pm_lazy_withdraw` | 销毁 shares → 本金 + pending;紧急惩罚留在池中 | + +在典型场景中池净赚 **+37 earned**(市场 M 收益 +20,杠杆 D 利息 +9,杠杆 E 对向下注回收 +8)。作为市场 LP 其 +本金无条件返还;只有*奖励*收益随争议变化。 + +> 池从单一 `free_balance` **同时**承担两种角色:市场 LP 分配(`maybe_allocate_lazy`)与杠杆贷款 +> (`leverage_fund_used` 限制后者)。所有杠杆参数都在 **`pm_leverage_open` 时**对当前中位值检查,故此后属性波动 +> 只影响*新*开仓,绝不影响已放出的贷款。 + +池中 VIZ 为**流动**而非 vested → **无**验证人调度或 committee-request 权重。**例外(HF14):** 对 **PM 委员会 +争议**,存款人的池权益**被计入**——经 `get_vesting_share_price()` 转为 vesting-shares 并加入其 `pm_dispute_vote` +权重(见下文委员会裁决者)。**核对:** `apply_hardfork(CHAIN_HARDFORK_14)`(单例)、`maybe_allocate_lazy`、 +`route_pool_lp_return`。**观察:** `get_lazy_pool`。 + +### 懒惰池中的流动性提供者(LZ1) + +**LZ1** 一次性向池存入 **1000**,任其在市场 + 杠杆贷款间分散。它赚取池聚合收益的份额(`reward_per_share`),而 +非任一市场的结果。两种退出:**计划**(锁定后)与**紧急**(锁定前,对*利润*罚一笔)。 + +```mermaid +flowchart LR + LZ1 -->|pm_lazy_deposit 1000| DEP[(pm_lazy_deposit_object
shares 1000, unlock=+7d)] + DEP --> POOL[(lazy pool)] + POOL -. yield accrues .-> RPS[reward_per_share ↑] + LZ1 -->|pm_lazy_withdraw| OUT{planned or
emergency?} + OUT -->|planned, t≥unlock| P[principal 1000 + pending 29] + OUT -->|emergency, t|pm_dispute_create proposed=B
escrow fee 1000| D[(pm_dispute_object
status open)] + D --> VOTE{committee vote
or account resolve} + VOTE ==>|overturn to B| FIN[[pm_dispute_finalize / resolve]] + FIN -->|fee 1000 + bonus 2000| disp + FIN -.slash 5000 from oracle.-> SPLIT[bonus 2000 → disp
3000 → forfeit_pool → winners] +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 争议,翻转(「胜」) | dispute_fee **1000** | fee 1000 退回 + **bonus 2000** | **+2000** | + +`reward_target = fee × pm_dispute_reward_multiplier (3×) = 3000` → `bonus = 3000 − 1000 = 2000`,**以实际罚没 +为上限**;余下(3000)→ `forfeit_pool` → 新赢家。disp 冒险 1000,最终 **+2000**。(委员会模式:disp **不**为自己 +投票——由 SHARES 选民投。)**核对:** `pm_dispute_create_evaluator`、`pm_dispute_finalize`/`pm_dispute_resolve` +的 overturn 分支。**观察:** `get_dispute`、`get_dispute_votes`。 + +### 争议者 —— 没收费用(预言机被维持) + +**disp** 争议预言机的 **A**,但裁决**维持预言机**。托管费用**没收给预言机**作为补偿,市场按原裁定结算(A 胜)。 + +```mermaid +flowchart LR + disp -->|pm_dispute_create proposed=B
escrow fee 1000| D[(pm_dispute_object)] + D --> VOTE{committee vote
or account resolve} + VOTE ==>|uphold oracle A| FIN[[pm_dispute_finalize / resolve]] + FIN -->|dispute_fee 1000| orac[oracle compensation] + FIN -->|market settles as A| AUTO[[pm_auto_payout]] +``` + +| 结果 | 发送 | 接收 | 净额 | +|------|------|------|------| +| 争议,维持(「负」) | dispute_fee **1000** | **0** | **−1000** | + +费用是争议者的利益绑定:错误/轻率的争议向预言机付费。这种不对称(错则失费、对则赢数倍)保持渠道诚实。一个始终 +**未决**的争议(预言机沉默 / 无法定人数)被强制关闭并**退还**费用(净 0)——见下文争议自动关闭,与按实质败诉不同。 +**核对:** `pm_dispute_finalize`/`pm_dispute_resolve` 的 uphold 分支。**观察:** `get_dispute`、`get_oracle` +(获得费用,`disputes_won++`)。 + +### 裁决者 —— 委员会(按权益加权,dispute_mode = 0) + +*整个 SHARES 选民*以**按权益加权投票**裁决;无单一裁决者账户。裁决在 `voting_end_time` 由 `pm_dispute_finalize` +确定性计票。 + +**投票权重** = 实时 **`effective_vesting_shares`**(`vesting − delegated + received`)**加上转为 vesting-shares +的懒惰池权益**,因为许多 DAO 成员把 VIZ 停在池里(在那里它是流动的): + +``` +pool_claim_viz = pool_NAV × deposit.shares / pool.total_shares +pool_weight = pool_claim_viz × get_vesting_share_price() +voter_weight = effective_vesting_shares + pool_weight +``` + +参与法定人数分母同样是 `total_vesting_shares + (pool_NAV → vesting-shares)`。7 天存款锁防止存款-投票-提取博弈。 + +```mermaid +flowchart LR + V1[voter · eff_vshares] -->|pm_dispute_vote outcome,percent| D[(pm_dispute_votes)] + V2[voter · eff_vshares] -->|pm_dispute_vote| D + D ==>|voting_end_time| FIN[[pm_dispute_finalize]] + FIN -->|argmax rshares,
threshold check| VERDICT{uphold / overturn} + VERDICT -->|consensus_strength
scales slash & bans| OUT[settle] +``` + +- **发送(投票者):** `pm_dispute_vote` —— **auth `regular`**。`vote_outcome = -1` 维持,否则提议正确结果; + `vote_percent ∈ [-10000, 10000]`。投票者在投票开放期间可**任意次修改**选票——重复投票**覆盖**先前的(最新者 + 胜,无「Already voted」)。 +- **无 commit-reveal —— 刻意,且不会改变。** 委员会争议是**公开听证**:实时计票可见(`get_dispute_votes`),票 + 不隐藏。DAO 的价值在于尽可能真实透明地裁决争议;投票中会浮现新论据,投票者*被期望*更新;且投票者**不因**与多数 + 一致而**获酬**,故 commit-reveal 通常的反羊群(选美博弈)理由在此不适用。 + +| 角色 | 发送 | 接收 | +|------|------|------| +| 每位投票者 | 0 | **0** —— 投票是治理职责,非有偿行为 | + +投票者从不收代币;影响力是纯权益权重。经济流向按上文争议主账本归于争议者、预言机与下注者。小众市场可能不达门槛 → +落入下文争议自动关闭。**核对:** `pm_dispute_vote_evaluator`(在 `by_market_voter` 上 modify-or-create); +`pm_dispute_finalize`(`lazy_vote_weight`、`get_vesting_share_price`、法定人数、argmax、`consensus_strength`)。 +**观察:** `get_dispute_votes`(实时计票 + finalize 投影:`quorum_percent_bp`、`expected_uphold`、 +`expected_outcome`、`expected_consensus_strength_bp`)。测试:`committee_dispute_lazy_pool_voting_weight`、 +`committee_dispute_flips_outcome`。 + +### 裁决者 —— 单账户(中心化,dispute_mode = 1) + +市场指名一个 `dispute_resolver` 账户(如监管多签)独自裁决——**无权益权重、无 DAO 投票**。在创建时设定,须与 +`oracle` 和 `creator` 都不同(防自裁)。操作集与委员会模式相同;只是*由谁裁决*不同。 + +```mermaid +flowchart LR + disp -->|pm_dispute_create| D[(dispute, mode=1)] + resolver -->|pm_dispute_resolve
correct_outcome=B
penalty_amount, ban flags| FIN[[verdict]] + FIN -->|slash penalty_amount| orac[oracle.insurance ↓] + FIN -->|fee + reward| disp + FIN --> AUTO[[pm_auto_payout settles B]] +``` + +- **发送:** `pm_dispute_resolve` —— **仅指名 `dispute_resolver` 的 `active` auth**:`correct_outcome`、 + `penalty_amount`(待罚没保证金——固定额,**不**按权益缩放)、`ban_oracle`/`ban_creator`(+ `…_until`)。 + +| 角色 | 发送 | 接收 | +|------|------|------| +| 裁决者 | 0 | **0** —— 中立仲裁者 | + +裁决后的规范与委员会模式相同;只是罚没规模不同(裁决者设定的 `penalty_amount`,无 `consensus_strength` 缩放, +因只有一个决策者)。裁决者的 KYC/白名单是**客户端层**事务。**核对:** `pm_dispute_resolve_evaluator`(仅指名裁 +决者,`dispute_mode==1`)。**观察:** `get_dispute`、`get_oracle`、**`get_creator_ban(account)`**。 + +### 争议被强制结束(防冻结自动关闭) + +一个始终**未决**的争议——预言机沉默且(委员会)无法定人数——不能永远冻结市场。在 `auto_close_time`, +`pm_dispute_auto_close` 处理器强制结束它:**全员退款**,争议者费用**退还**,无响应的预言机受罚。不选赢家。 + +```mermaid +flowchart LR + disp -->|pm_dispute_create
escrow fee 1000| D[(dispute, status open)] + D -. oracle silent / no quorum .-> WAIT[auto_close_time reached] + WAIT ==>|VIRTUAL| AC[[pm_dispute_auto_close]] + AC -->|refund all bets| bettors + AC -->|fee 1000 back| disp + AC -->|insurance slash → DAO| orac +``` + +| 角色 | 发送 | 接收 | 净额 | +|------|------|------|------| +| A / B / C | 下注 | 全额退款 | **0** | +| maker / LP1 | 流动性 | 本金退回 | **0**(无奖励) | +| disp | dispute_fee 1000 | **退回 1000** | **0** | +| orac | 保证金 −罚没 → DAO | — | **− 罚没** | + +这**不是**「争议者输了」:退回的费用(净 0)不同于被没收的费用(争议者输家,净 −1000)。无人获利;市场作废以打破 +冻结,代价落在未响应的预言机上。同样的作废-退款形态也覆盖 `pm_oracle_missed_penalty` 与 `pm_no_contest`。调校 +`pm_dispute_auto_close_sec`(14 天)对 `pm_dispute_vote_period_sec`(3 天),使诚实争议先行解决。**核对:** +`process_pm_markets` 自动关闭扫描(`refund_all_bets` + `return_liquidity` + 费用计入;`disputes_auto_closed++`)。 +**观察:** `get_dispute`(状态 → 自动关闭)、`get_market`、`get_oracle`。 diff --git a/@l10n/zh-CN/docs/protocol/operations/overview.md b/@l10n/zh-CN/docs/protocol/operations/overview.md index 602109cc12..31dcf15306 100644 --- a/@l10n/zh-CN/docs/protocol/operations/overview.md +++ b/@l10n/zh-CN/docs/protocol/operations/overview.md @@ -52,6 +52,32 @@ VIZ Ledger 操作是包含在交易中的原子状态变更动作。每个操作 | 58 | `use_invite_balance_operation` | active | [邀请](./invites.md) | | 60 | `fixed_award_operation` | regular | [奖励](./awards.md) | | 61 | `target_account_sale_operation` | master | [账户市场](./account-market.md) | +| 64 | `set_reward_sharing_operation` | active | [验证者](./validators.md) | +| 66 | `pm_oracle_register_operation` | active | [预测市场](./prediction-markets.md) | +| 67 | `pm_oracle_update_operation` | active | [预测市场](./prediction-markets.md) | +| 68 | `pm_create_market_operation` | active | [预测市场](./prediction-markets.md) | +| 69 | `pm_oracle_accept_market_operation` | active | [预测市场](./prediction-markets.md) | +| 70 | `pm_place_bet_operation` | active | [预测市场](./prediction-markets.md) | +| 71 | `pm_commit_bet_operation` | active | [预测市场](./prediction-markets.md) | +| 72 | `pm_reveal_bet_operation` | active | [预测市场](./prediction-markets.md) | +| 73 | `pm_cancel_bet_operation` | active | [预测市场](./prediction-markets.md) | +| 74 | `pm_add_liquidity_operation` | active | [预测市场](./prediction-markets.md) | +| 75 | `pm_withdraw_liquidity_operation` | active | [预测市场](./prediction-markets.md) | +| 76 | `pm_resolve_market_operation` | active | [预测市场](./prediction-markets.md) | +| 77 | `pm_no_contest_operation` | active | [预测市场](./prediction-markets.md) | +| 78 | `pm_dispute_create_operation` | active | [预测市场](./prediction-markets.md) | +| 79 | `pm_dispute_vote_operation` | regular | [预测市场](./prediction-markets.md) | +| 80 | `pm_dispute_resolve_operation` | active | [预测市场](./prediction-markets.md) | +| 81 | `pm_transfer_position_operation` | active | [预测市场](./prediction-markets.md) | +| 82 | `pm_lazy_deposit_operation` | active | [预测市场](./prediction-markets.md) | +| 83 | `pm_lazy_withdraw_operation` | active | [预测市场](./prediction-markets.md) | +| 91 | `pm_leverage_open_operation` | active | [预测市场](./prediction-markets.md) | +| 92 | `pm_leverage_close_operation` | active | [预测市场](./prediction-markets.md) | +| 93 | `pm_leverage_convert_operation` | active | [预测市场](./prediction-markets.md) | +| 98 | `pm_dispute_oracle_respond_operation` | active | [预测市场](./prediction-markets.md) | +| 99 | `pm_unban_operation` | active | [预测市场](./prediction-markets.md) | + +> ID 是链上单一 `operation` 变体中的固定索引(仅追加)。本表中的空缺为按 ID 交错的**虚拟**操作(见下文)—— 例如 62–63、65、84–90、94–97、100。 --- @@ -83,6 +109,19 @@ VIZ Ledger 操作是包含在交易中的原子状态变更动作。每个操作 | 59 | `expire_escrow_ratification_operation` | 托管截止日期错过 | [虚拟操作](../virtual-operations.md) | | 62 | `bid_operation` | 拍卖出价 | [虚拟操作](../virtual-operations.md) | | 63 | `outbid_operation` | 拍卖被超价 | [虚拟操作](../virtual-operations.md) | +| 65 | `stakeholder_reward_operation` | 向利益相关者的收益分成结算 | [验证者](./validators.md) | +| 84 | `pm_batch_settle_operation` | 批量纪元边界结算 | [预测市场](./prediction-markets.md) | +| 85 | `pm_commit_forfeit_operation` | 提交-揭示托管被没收(未揭示) | [预测市场](./prediction-markets.md) | +| 86 | `pm_auto_payout_operation` | 市场结算(按市场赔付标记) | [预测市场](./prediction-markets.md) | +| 87 | `pm_dispute_finalize_operation` | 委员会投票计票完成 | [预测市场](./prediction-markets.md) | +| 88 | `pm_dispute_auto_close_operation` | 争议防冻结自动关闭 | [预测市场](./prediction-markets.md) | +| 89 | `pm_oracle_missed_penalty_operation` | 预言机错过裁定截止 | [预测市场](./prediction-markets.md) | +| 90 | `pm_lazy_recall_operation` | 懒惰池渐进式召回步 | [预测市场](./prediction-markets.md) | +| 94 | `pm_leverage_liquidate_operation` | 杠杆头寸被清算 | [预测市场](./prediction-markets.md) | +| 95 | `pm_leverage_resolve_operation` | 杠杆头寸在裁定时结算 | [预测市场](./prediction-markets.md) | +| 96 | `pm_market_accepted_operation` | 市场上线(预言机接受 / 自预言机 / 自动) | [预测市场](./prediction-markets.md) | +| 97 | `pm_payout_operation` | 每下注者的同注分彩赔付 | [预测市场](./prediction-markets.md) | +| 100 | `pm_ban_expired_operation` | 临时预言机/创建者封禁失效 | [预测市场](./prediction-markets.md) | --- diff --git a/@l10n/zh-CN/docs/protocol/operations/prediction-markets.md b/@l10n/zh-CN/docs/protocol/operations/prediction-markets.md index 25501781d3..03ec881778 100644 --- a/@l10n/zh-CN/docs/protocol/operations/prediction-markets.md +++ b/@l10n/zh-CN/docs/protocol/operations/prediction-markets.md @@ -106,6 +106,8 @@ flowchart TD 预言机接受(`status → active`)或拒绝(流动性退还创建者;`status → deleted`)一个待定市场。接受时预言机通过 `oracle_fee_percent` + `oracle_fixed_fee` **报出其实际条款**——各须 `≤` 该市场创建者的报价,且 `oracle_fee_percent ≤ pm_max_oracle_fee_percent`。报价被**冻结入市场**并发出 `pm_market_accepted` 虚拟操作(使历史解析器看到上线 + 条款)。此后结算只读这些冻结字段——绝不读实时中位值。 +预言机须在创建后的 `pm_oracle_accept_window_sec`(默认 1 小时)内行动。若到市场的 `accept_deadline` 仍未接受或拒绝,逐块 cron 将作废市场(`status → deleted`),向创建者退还种子流动性(**不**含不可退还的创建费),并发出 `pm_market_expired`(见虚拟操作)。 + | 字段 | 类型 | 说明 | |------|------|------| | `market_id` | `int64` | 待定市场 | @@ -155,7 +157,14 @@ flowchart TD ### `pm_resolve_market_operation`(ID 76) **Auth:** `oracle` 的 `active` -预言机裁定为 `winning_outcome`。开启争议宽限窗口(`result_expiration + pm_dispute_grace_sec`);其过后由 `pm_auto_payout` 结算。 +预言机裁定为 `winning_outcome`。开启争议宽限窗口(`result_expiration + pm_dispute_grace_sec`);其过后由 `pm_auto_payout` 结算。预言机的裁定声明**存储在市场上**(如同预言机的 `rules_url`),因此客户端可直接通过 `get_market` 读取,无需扫描历史。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `market_id` | `int64` | 目标市场 | +| `winning_outcome` | `int16_t` | 获胜结果索引 | +| `decision_url` | `string` | 证据链接,`≤ MAX_PM_DECISION_URL_LEN`;存储在市场上 | +| `decision_reason` | `string` | 自由文本理由,`≤ MAX_PM_DISPUTE_REASON_LEN`;存储在市场上(`decision_reason`) | ### `pm_no_contest_operation`(ID 77) **Auth:** `oracle` 的 `active` @@ -177,7 +186,39 @@ flowchart TD ### `pm_dispute_resolve_operation`(ID 80) **Auth:** `resolver` 的 `active` -由市场配置的 `dispute_resolver` 做出账户模式裁决。可罚没 `penalty_amount` 的保证金,并将预言机/创建者封禁至给定时间。 +由市场配置的 `dispute_resolver` 做出账户模式裁决。可罚没 `penalty_amount` 的保证金,并将预言机/创建者封禁至给定时间(`ban_*_until = time_point_sec::maximum()` = 永久)。 + +> **封禁是一项合规/监管功能,仅限账户模式。** 当市场将其争议路由到账户模式的 `dispute_resolver`(例如监管机构或持牌仲裁者)时,该解析者可在同一裁决中对**预言机与市场创建者双方**施加处罚——临时或永久——在保证金罚没之外:这让担任解析者的监管机构得以将恶意预言机或屡犯的创建者逐出平台。**委员会/DAO 模式(`dispute_mode == 0`)按设计没有封禁权**——它是透明的公开听证,只罚没保证金并调整声誉(`pm_dispute_finalize`),绝不封禁。此处设定的封禁会将发起的 `resolver` 记入目标的 `banned_by`,因此只有该解析者可通过 `pm_unban` 提前解除;否则封禁在 `banned_until` 时失效(cron 发出 `pm_ban_expired`)。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `market_id` | `int64` | 争议市场 | +| `correct_outcome` | `int16_t` | 最终正确结果(`-1` = 作废/无争议) | +| `penalty_amount` | `asset`(VIZ) | 罚没的预言机保证金 | +| `ban_oracle` / `ban_oracle_until` | `bool` / `time_point_sec` | 将预言机封禁至给定时间 | +| `ban_creator` / `ban_creator_until` | `bool` / `time_point_sec` | 将创建者封禁至给定时间(禁止创建市场) | + +### `pm_dispute_oracle_respond_operation`(ID 98) +**Auth:** `oracle` 的 `active` + +市场的预言机在开放的争议上发布**公开反驳**。由于争议是公开听证,文本存储在争议对象上(`oracle_response` / `oracle_response_time`,可通过 `get_dispute` 读取),以便每位投票者/解析者权衡。仅在争议开放且 `now ≤ oracle_response_deadline` 时允许;重新发布会覆盖先前响应。 + +| 字段 | 类型 | 说明 | +|------|------|------| +| `market_id` | `int64` | 争议市场 | +| `response` | `string` | 反驳文本,非空,`≤ MAX_PM_DISPUTE_REASON_LEN` | + +### `pm_unban_operation`(ID 99) +**Auth:** `resolver` 的 `active` + +**提前**解除由账户模式 `pm_dispute_resolve` 施加的封禁。仅目标的 `banned_by` 中记录的账户(施加封禁的解析者)可解除;`unban_oracle` / `unban_creator` 中至少须设定一个,且对应封禁当前须处于活跃状态。将 `banned_until` 设为过去时间并清空 `banned_by`。(未在此解除的封禁只会在 `banned_until` 时到期——届时 cron 发出 `pm_ban_expired`。) + +| 字段 | 类型 | 说明 | +|------|------|------| +| `resolver` | `account_name_type` | 施加封禁的账户(须等于目标的 `banned_by`) | +| `target` | `account_name_type` | 被封禁的预言机 / 创建者 | +| `unban_oracle` | `bool` | 清除预言机封禁(`pm_oracle_object.banned_until`) | +| `unban_creator` | `bool` | 清除创建者封禁(`pm_creator_ban_object.banned_until`) | ### `pm_transfer_position_operation`(ID 81) **Auth:** `from` 的 `active` @@ -213,6 +254,12 @@ flowchart TD | 95 | `pm_leverage_resolve_operation` | 结算——杠杆头寸被强制关闭:`outcome_index`、`won`、`pool_received`/`bettor_received`、`leverage` | | 96 | `pm_market_accepted_operation` | 求值器——市场上线:预言机接受、自预言机或自动接受;冻结条款 + `self_oracle` 标志 | | 97 | `pm_payout_operation` | 结算——每个有效下注:`amount`(本金)、`side`/`outcome_index`、`payout`(**输则为 0**) | +| 100 | `pm_ban_expired_operation` | 临时的预言机/创建者封禁在 `banned_until` 时失效:cron 将其清除(字段 `account`、`oracle`、`creator`)。提前手动解除改用已签名的 `pm_unban` | +| 101 | `pm_market_expired_operation` | 待定市场的 `accept_deadline` 到期:预言机未在 `pm_oracle_accept_window_sec` 内接受/拒绝——市场作废,种子退还(创建费保留)。字段 `oracle`、`creator`、`market_id`、`refunded_liquidity` | + +> ID 91–93 是*常规*操作 `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert`(见规范); +> ID 98–99 是*常规*操作 `pm_dispute_oracle_respond`/`pm_unban`(见上)。每位下注者的结果是 +> `pm_payout`;每市场的 `pm_auto_payout` 仍为结算标记。 --- diff --git a/@l10n/zh-CN/docs/protocol/operations/validators.md b/@l10n/zh-CN/docs/protocol/operations/validators.md index 7c83b51228..d7eb913e1d 100644 --- a/@l10n/zh-CN/docs/protocol/operations/validators.md +++ b/@l10n/zh-CN/docs/protocol/operations/validators.md @@ -163,4 +163,39 @@ --- +## `set_reward_sharing_operation`(ID 64) + +**授权:** `owner` 的 `active` + +**HF13 验证者收益分成。** 验证者选择将其区块奖励的一部分转发给其**利益相关者**——即为其投票的账户——按时间加权的投票权重比例分配。`sharing_rate` 为该比例,以基点表示;分成池累积后在每个纪元结束时通过 `stakeholder_reward` 虚拟操作分配。 + +| 字段 | 类型 | 描述 | +|------|------|------| +| `owner` | `account_name_type` | 设置分成比例的验证者 | +| `sharing_rate` | `uint16_t` | 转发给利益相关者的区块奖励比例,以基点表示(0 = 无,10000 = 100%) | + +```json +[64, { + "owner": "alice", + "sharing_rate": 2500 +}] +``` + +- `sharing_rate` 上限为 10000(100%)。 +- 分配按**时间加权**的投票权重进行,因此新添加的投票在成熟前获得较小的份额。 + +--- + +## `stakeholder_reward_operation`(ID 65)— 虚拟 + +在每个分配纪元,当 `sharing_rate` 非零的验证者向利益相关者支付其应得的分成区块奖励时发出。虚拟操作(从不签名);出现在 `account_history` 中。 + +| 字段 | 类型 | 描述 | +|------|------|------| +| `validator` | `account_name_type` | 分享奖励的验证者 | +| `stakeholder` | `account_name_type` | 接收份额的投票者 | +| `shares` | `asset`(SHARES) | 记入利益相关者的金额 | + +--- + 参见:[数据类型](../data-types.md)、[操作概述](./overview.md)、[链属性](../../governance/chain-properties.md)。 diff --git a/@l10n/zh-CN/docs/protocol/virtual-operations.md b/@l10n/zh-CN/docs/protocol/virtual-operations.md index 843380b33d..9c22fd7ff3 100644 --- a/@l10n/zh-CN/docs/protocol/virtual-operations.md +++ b/@l10n/zh-CN/docs/protocol/virtual-operations.md @@ -351,4 +351,32 @@ --- +## 预测市场 (HF14) + +由 PM 共识逻辑发出 —— **并非**按时钟的 cron。两个来源: +- **已签名操作的求值器**,在其应用的瞬间 —— `pm_market_accepted`(接受 / 自预言机 / 自动接受)与 `pm_leverage_liquidate`(对向或取消下注将杠杆头寸推过阈值时)。 +- **截止处理器 `process_pm_markets()`**,每块运行:结算已达到**到期 / 截止 / 争议宽限结束 / 纪元边界**的市场(上限 `pm_processing_cap_per_block`,最早截止优先)。 + +参见 [预测市场操作](./operations/prediction-markets.md)。(ID 91–93 是*常规*操作 `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert`,ID 98–99 是*常规*操作 `pm_dispute_oracle_respond`/`pm_unban` —— 均见该页。) + +| ID | 操作 | 触发 | +|----|------|------| +| 84 | `pm_batch_settle_operation` | 到达纪元边界:排队下注按纪元开盘快照执行 | +| 85 | `pm_commit_forfeit_operation` | `reveal_deadline` 未揭示:罚金 → `forfeit_pool`,其余退还 | +| 86 | `pm_auto_payout_operation` | 争议宽限到期(按市场):同注分彩结算 + 返还 LP 本金 | +| 87 | `pm_dispute_finalize_operation` | 到达 `voting_end_time`:计票裁定;预言机罚金;重判/维持 | +| 88 | `pm_dispute_auto_close_operation` | 到达 `auto_close_time` 且预言机未响应:防冻结退款,罚没保证金 → DAO | +| 89 | `pm_oracle_missed_penalty_operation` | `result_expiration` 已过仍未裁决:罚没 → DAO,全额退还下注 | +| 90 | `pm_lazy_recall_operation` | 闲置的懒惰池分配到达回收步:一个分阶段步返还入池 | +| 94 | `pm_leverage_liquidate_operation` | 求值器 —— 市场进行中的杠杆清算(对向 `0` / 取消 `1` 下注级联) | +| 95 | `pm_leverage_resolve_operation` | 结算 —— 杠杆头寸按 `cancel_value` 强制关闭:`outcome_index`、`won`、`pool_received`/`bettor_received`、`leverage` | +| 96 | `pm_market_accepted_operation` | 求值器 —— 市场上线:预言机接受、自预言机或自动接受;冻结条款 + `self_oracle` 标志 | +| 97 | `pm_payout_operation` | 结算 —— 每个有效下注:`amount`(本金)、`side`/`outcome_index`、`payout`(**输则为 0**);与按市场的 `pm_auto_payout` 并列 | +| 100 | `pm_ban_expired_operation` | 临时预言机/创建者封禁在 `banned_until` 失效:cron 将其清除(`account`、`oracle`、`creator`)。提前手动解除改用已签名的 `pm_unban` | +| 101 | `pm_market_expired_operation` | 待定市场的 `accept_deadline` 到期:预言机未在 `pm_oracle_accept_window_sec` 内接受/拒绝——市场作废(`status -1`),种子流动性退还(`refunded_liquidity`),创建费**不**退还(`oracle`、`creator`、`market_id`、`refunded_liquidity`) | + +所有 PM 资金流动严格零和(无增发);结算守恒 `Σ out == Σ 下注 + LP 本金 + forfeit_pool`。 + +--- + 参见:[操作概述](./operations/overview.md)、[奖励](./operations/awards.md)、[委员会](./operations/committee.md)。 diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 11cb8fd882..054c82bdd8 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -1,4 +1,5 @@ import { defineConfig, type DefaultTheme } from 'vitepress'; +import { withMermaid } from 'vitepress-plugin-mermaid'; const githubRepo = 'https://github.com/VIZ-Blockchain/viz-cpp-node'; @@ -51,6 +52,16 @@ interface SidebarLabels { awards: string; subscriptions: string; accountMarket: string; + predictionMarkets: string; + predictionMarketApi: string; + predictionMarketsSection: string; + onixOverview: string; + onixWhitepaper: string; + onixSpecification: string; + pmHub: string; + pmGuides: string; + pmWorkflows: string; + pmConcepts: string; proposals: string; storage: string; sharedMemory: string; @@ -126,6 +137,16 @@ const en: SidebarLabels = { awards: 'Awards', subscriptions: 'Subscriptions', accountMarket: 'Account Market', + predictionMarkets: 'Prediction Markets', + predictionMarketApi: 'Prediction Market API', + predictionMarketsSection: 'Prediction Markets (Onix)', + onixOverview: 'Onix Overview', + onixWhitepaper: 'Whitepaper', + onixSpecification: 'Specification', + pmHub: 'Overview & map', + pmGuides: 'Guides — by role and feature', + pmWorkflows: 'Workflows & diagrams', + pmConcepts: 'Concept analysis', proposals: 'Proposals', storage: 'Storage', sharedMemory: 'Shared Memory', @@ -201,6 +222,16 @@ const ru: SidebarLabels = { awards: 'Награды', subscriptions: 'Подписки', accountMarket: 'Рынок аккаунтов', + predictionMarkets: 'Прогнозные рынки', + predictionMarketApi: 'API прогнозных рынков', + predictionMarketsSection: 'Прогнозные рынки (Onix)', + onixOverview: 'Обзор Onix', + onixWhitepaper: 'Whitepaper', + onixSpecification: 'Спецификация', + pmHub: 'Обзор и карта', + pmGuides: 'Гайды — по ролям и фичам', + pmWorkflows: 'Воркфлоу и диаграммы', + pmConcepts: 'Анализ концептов', proposals: 'Предложения', storage: 'Хранилище', sharedMemory: 'Разделяемая память', @@ -276,6 +307,16 @@ const zhCN: SidebarLabels = { awards: '奖励', subscriptions: '订阅', accountMarket: '账户市场', + predictionMarkets: '预测市场', + predictionMarketApi: '预测市场 API', + predictionMarketsSection: '预测市场(Onix)', + onixOverview: 'Onix 概览', + onixWhitepaper: '白皮书', + onixSpecification: '规范', + pmHub: '总览与地图', + pmGuides: '指南 — 按角色与功能', + pmWorkflows: '工作流与图', + pmConcepts: '概念分析', proposals: '提案', storage: '存储', sharedMemory: '共享内存', @@ -317,6 +358,21 @@ function buildSidebar(t: SidebarLabels, prefix: string): DefaultTheme.SidebarIte { text: t.keyConcepts, link: p('/introduction/key-concepts') }, ], }, + { + text: t.predictionMarketsSection, + items: [ + { text: t.pmHub, link: p('/prediction-markets/') }, + { text: t.pmGuides, link: p('/prediction-markets/guides/') }, + { text: t.onixOverview, link: p('/prediction-markets/onix') }, + { text: t.onixWhitepaper, link: p('/prediction-markets/whitepaper') }, + { text: t.onixSpecification, link: p('/prediction-markets/specification') }, + { text: t.predictionMarkets, link: p('/protocol/operations/prediction-markets') }, + { text: t.virtualOperations, link: p('/protocol/virtual-operations') }, + { text: t.predictionMarketApi, link: p('/plugins/prediction-market-api') }, + { text: t.pmWorkflows, link: p('/prediction-markets/workflows') }, + { text: t.pmConcepts, link: p('/prediction-markets/concepts-analysis') }, + ], + }, { text: t.runANode, items: [ @@ -358,6 +414,7 @@ function buildSidebar(t: SidebarLabels, prefix: string): DefaultTheme.SidebarIte { text: t.snapshot, link: p('/plugins/snapshot') }, { text: t.chain, link: p('/plugins/chain') }, { text: t.databaseApi, link: p('/plugins/database-api') }, + { text: t.predictionMarketApi, link: p('/plugins/prediction-market-api') }, { text: t.webserver, link: p('/plugins/webserver') }, ], }, @@ -382,6 +439,7 @@ function buildSidebar(t: SidebarLabels, prefix: string): DefaultTheme.SidebarIte { text: t.awards, link: p('/protocol/operations/awards') }, { text: t.subscriptions, link: p('/protocol/operations/subscriptions') }, { text: t.accountMarket, link: p('/protocol/operations/account-market') }, + { text: t.predictionMarkets, link: p('/protocol/operations/prediction-markets') }, { text: t.proposals, link: p('/protocol/operations/proposals') }, ], }, @@ -451,6 +509,7 @@ function localizedNav( const p = (path: string) => `${prefix}${path}`; return [ { text: introLabel, link: p('/introduction/what-is-viz') }, + { text: 'Onix', link: p('/prediction-markets/') }, { text: nodeLabel, link: p('/node/getting-started') }, { text: protocolLabel, link: p('/protocol/data-types') }, { text: apiLabel, link: p('/api/json-rpc') }, @@ -460,7 +519,7 @@ function localizedNav( // ─── Config ───────────────────────────────────────────────────────────────── -export default defineConfig({ +export default withMermaid(defineConfig({ title: 'VIZ Ledger Documentation', description: 'Official documentation for VIZ Ledger — hybrid DLT with Fair-DPOS consensus', base: '/viz-cpp-node/', @@ -505,4 +564,4 @@ export default defineConfig({ }, }, }, -}); +})); diff --git a/docs/TECH_DEBT_AUDIT.md b/docs/TECH_DEBT_AUDIT.md index b29b96102b..0b756ed44b 100644 --- a/docs/TECH_DEBT_AUDIT.md +++ b/docs/TECH_DEBT_AUDIT.md @@ -1,229 +1,157 @@ # Tech Debt Audit — viz-cpp-node -Generated: 2026-05-01 -Branch audited: `chore/dead-code-tier-a` -Scope: source under `libraries/`, `plugins/`, `programs/`, root `CMakeLists.txt`, `documentation/`, `share/vizd/`. Excludes `thirdparty/` (vendored submodules: fc, chainbase, appbase). - -> ## Refresh — 2026-06-17 (verified against working tree) -> -> The May findings below are kept as a historical log. The current state of each verified item: -> -> **Fixed since May:** -> - **F008** — `CHAIN_INTERNAL_PLUGINS` lines removed from `plugins/CMakeLists.txt`. -> - **F009** — `test_api_plugin` no longer registered in `programs/vizd/main.cpp`. -> - **F017 / F029** — `chain_test` reference removed from `documentation/building.md`. -> - **F046 / F047 / F048** — `documentation/plugin.md` no longer references the phantom dirs, `newplugin.py`, or the `CHAIN_INTERNAL_PLUGINS` claim. -> - **F051** — `cat_parts.py` now uses `return False` / `return True`. -> -> **Partially addressed:** -> - **F002** — `libraries/network/node.cpp` was split; the file is gone, replaced by `dlt_p2p_node.cpp` (4,113 LOC) + `dlt_p2p_messages.cpp` (26 LOC). The message extraction was trivial — the 4,113-LOC `dlt_p2p_node.cpp` is still a god file. F039's line refs (`node.cpp:4192-4395`) now point into `dlt_p2p_node.cpp`. -> - **F019** — `remote_node_api.hpp` reduced from 10 `using namespace plugins::*` to 5. Still present. -> -> **Still open (line/LOC drift):** -> - **F001** — `database.cpp` grew to **7,975 LOC** (+1,470, much of it stall-monitor work). Still #1 split priority. F036/F037 TODO line refs have shifted. -> - **F003** — `plugins/snapshot/plugin.cpp` grew to **4,345 LOC** (+1,068). Not split. (Note: path is `plugins/snapshot/`, not `libraries/chain/snapshot/`.) -> - **F004** wallet.cpp 2,796 · **F005** chain_evaluator.cpp 2,350 · **F006** wallet.hpp 1,554 — roughly unchanged. -> - **F019** — `remote_node_api.hpp` reduced to 5 `using namespace plugins::*`. **Deferred:** narrowing them needs a local compiler — dozens of bare types resolve through those usings (incl. bare `set<>` with no `using std::set`), so a blind edit would break the build across consumers. -> -> **Fixed in working tree (pending CI build):** -> - **F018** — `using namespace std;` in `wallet.hpp` → targeted `using std::{string,vector,map,pair};`. -> - **F041** — `std::exit(0)` ×2 at `plugins/chain/plugin.cpp:672,698` → `appbase::app().quit()`. (Fixes the bypassed-shutdown bug; quit() likely still exits 0, so not an exit-code change.) -> -> **⚠️ Obsolete / now backwards — DO NOT ACT ON:** -> - **F012** — The original `std::cerr` targets (`database.cpp:398,509`, reindex progress) are gone. The 5 remaining `std::cerr` calls (lines 329, 701, 828, 1160, 1174) are the **deliberate stall-watchdog** stderr — written to `cerr` precisely *so the monitor can never block on node locks*. Converting them to `ilog`/`wlog` would defeat the watchdog. **This recommendation is now wrong; leave the watchdog stderr as-is.** F011/F038/F045 snapshot `cerr` line refs have also drifted (file is now 4,345 LOC). -## Executive summary - -- **God-file concentration is the single biggest structural problem.** Five files (`libraries/chain/database.cpp` 6,505 LOC; `libraries/network/node.cpp` 5,759 LOC; `plugins/snapshot/plugin.cpp` 3,277 LOC; `libraries/wallet/wallet.cpp` 2,886 LOC; `libraries/chain/chain_evaluator.cpp` 2,347 LOC) hold ~20.8k LOC, roughly 40% of the real code surface. Three of these five are also the highest-churn files in the last 6 months (snapshot 55, database 38, node 25 commits). Size × churn = where bugs concentrate. -- **Zero unit tests.** No `tests/` directory anywhere. No `BOOST_AUTO_TEST_CASE` callsites in non-thirdparty code. `documentation/building.md:374` advertises `chain_test` as a build target but no such target exists. `documentation/testing.md` describes a testing process for a system that isn't there. -- **Documentation drift is systemic, not episodic.** `documentation/plugin.md` references three directories that don't exist (`libraries/plugins`, `external_plugins`, `example_plugins`) and a file that doesn't exist (`programs/util/newplugin.py`). It also describes runtime behavior (`CHAIN_INTERNAL_PLUGINS` "used by argument parsing") that is fictional — the variable is set but never read. -- **`test_api_plugin` is registered into production `vizd`** (`programs/vizd/main.cpp:72`). Its `test_api_a` / `test_api_b` JSON-RPC methods return hardcoded `"A"` and `"B"`. It's gated on `enable-plugin` config, but live in the production binary's plugin registry. -- **`using namespace` in public headers, including `using namespace std;`** in `libraries/wallet/include/graphene/wallet/wallet.hpp:16`. Pollutes every consumer of the wallet API. `libraries/wallet/include/graphene/wallet/remote_node_api.hpp:28-38` adds 10 more — a wallet-side dependency rats-nest. -- **Two implementations of the hardfork concatenator** (`programs/build_helpers/cat-parts.cpp` for MSVC, `programs/build_helpers/cat_parts.py` for everything else). The Python version has latent NameError bugs (`return false` lowercase on lines 14, 16); they are unreachable today only because `hardfork.d` happens to contain only regular `.hf` files. -- **Plugin glob with dead state.** `plugins/CMakeLists.txt:2-12` walks subdirectories and appends each to `CHAIN_INTERNAL_PLUGINS`, but no other CMake or C++ file ever reads that variable. Pure ceremony. -- **Console I/O leaks across the logging boundary.** ~75 direct `std::cerr` / `std::cout` / `printf` callsites in libraries+plugins despite `fc::ilog/wlog/elog` being the project standard. `plugins/snapshot/plugin.cpp` has 40+ `std::cerr` callsites alone, with bespoke ANSI-color macros (CLOG_GREEN/ORANGE/etc., lines 50-54) that bypass fc entirely. -- **Submodules track branches, not tags.** All three (`fc`, `chainbase`, `appbase` in `.gitmodules`) point at moving branches. Reproducible builds from an old commit can silently change. -- **Layered cleanup work already in flight.** PR #100 (`chore/dead-code-tier-a`) is removing orphan files, dead CMake plumbing, and `#if 0` blocks. This audit complements it: many of the medium-effort architectural items below are out of scope for that PR but are good Tier B candidates. - -## Architectural mental model - -VIZ is a Graphene-derived blockchain (sister to Steem/Hive/BitShares lineage) with a Fair-DPOS consensus tweak. The codebase has a clean four-layer shape on paper: +**Generated:** 2026-08-11 (full re-audit against `master` @ `d4fe3334`) +**Scope:** source under `libraries/`, `plugins/`, `programs/`, `tests/`, root `CMakeLists.txt`, `.github/workflows/`, `share/vizd/`. Excludes `thirdparty/` (vendored submodules: fc, chainbase, appbase). -1. **`thirdparty/`** — vendored as submodules: `fc` (FC framework: serialization, logging, exceptions, async), `chainbase` (memory-mapped object database), `appbase` (plugin lifecycle / appbase pattern). All are VIZ-Blockchain forks tracking `update` / `lib-boost-1.71` branches. -2. **`libraries/`** — `protocol` (operations, types, asset), `chain` (state machine, evaluators, hardforks), `network` (P2P, peer_connection), `api` (RPC API helpers), `wallet` (cli_wallet model + remote node API), `time`, `utilities` (BIP39 wordlist, key derivation, plain_keys). -3. **`plugins/`** — ~25 plugins (account_history, snapshot, witness, p2p, json_rpc, webserver, mongo_db, etc.), all loaded via appbase. Two flavors: data plugins that index chain state, and API plugins that expose JSON-RPC methods. -4. **`programs/`** — `vizd` (production binary, registers ~20 plugins in `main.cpp:62-88`), `cli_wallet` (interactive wallet CLI), `util` (sign_digest, test_block_log, test_shared_mem — operator/dev tools), `build_helpers` (cat-parts hardfork concatenator, configure_build.py for Win cross-compile). +> This is a from-scratch re-audit that **replaces** the 2026-05-01 audit and its +> 2026-06-17 refresh. Those described a tree that has since drifted materially — +> e.g. they claimed "zero tests / no `tests/` directory" (a `consensus_sim` +> harness now exists), referenced `libraries/network/node.cpp` (renamed to +> `dlt_p2p_node.cpp`), and audited a `documentation/` tree that a pending PR +> removes. The old finding IDs (F001–F051) are **not** carried forward; this +> document uses a fresh `D###` numbering. Where a current item maps to prior +> work it is called out inline. -**Where the model breaks down:** - -The clean four-layer story is undermined by god-file concentration. `database.cpp` is the actual heart of the system — block push, fork resolution, undo session management, snapshot integration, hardfork application — all in one 6,500-line file. `network/node.cpp` is similarly omnibus: peer discovery, message routing, sync logic, and peer database all live together in 5,759 lines. Logical separation exists (peer_connection.cpp, message_oriented_connection.cpp are split out) but the bulk of behavior pools in the giants. +## Executive summary -The plugin layer has two architectural inconsistencies. First, `plugins/CMakeLists.txt` uses a glob loop and an env var (`CHAIN_INTERNAL_PLUGINS`) that nothing reads — the system claims plugins are auto-discovered but `programs/vizd/main.cpp:62-88` actually hardcodes the registration list. Second, `test_api_plugin` is in that hardcoded list and ships in production. +- **God-file concentration is still the dominant structural risk, and it now + coincides exactly with the highest churn.** The three biggest hand-written + translation units are also the three most-changed files in the last six + months: `libraries/chain/database.cpp` (7,991 LOC, 130 commits/6mo), + `plugins/snapshot/plugin.cpp` (4,657 LOC, 125 commits/6mo), and + `libraries/network/dlt_p2p_node.cpp` (4,380 LOC, 125 commits/6mo). Size × + churn is where consensus bugs concentrate. Together with `wallet.cpp` (2,796) + and `chain_evaluator.cpp` (2,350) these five hold ~22k LOC. +- **Tests now exist but are not enforced.** `tests/consensus_sim/` is a real + deterministic multi-node harness (32 `BOOST_AUTO_TEST_CASE` scenarios across 10 + files: determinism replay, equivocation, wedge predicate, smoke) built under + ASAN + UBSAN. **But it is gated `OFF` by default (`BUILD_CONSENSUS_TESTS`) and + no CI workflow builds or runs it.** The correctness gate is written but not + wired in. This is the single highest-value open item. +- **The build system does no optimization by default.** There is no default + `CMAKE_BUILD_TYPE`, and `-O3/-O2` are set only inside the MinGW branch. A bare + `cmake ..` (or an IDE configure) produces an unoptimized node. *(Addressed in + open PR #146.)* +- **No precompiled headers despite a 3.16 minimum.** Every TU re-parses the same + heavy Boost.MultiIndex / FC reflection headers. *(Opt-in PCH proposed in open + PR #148.)* +- **Massive CMake duplication.** 26 library/plugin `CMakeLists.txt` carry the + full source list twice, once per `SHARED`/`STATIC` branch. *(Collapsed in open + PR #147, which also fixes a real drift in the chain lib's two branches.)* +- **`using namespace` in public headers is widespread — 51 occurrences**, + including `using namespace std;` in `plugins/account_by_key/.../account_by_key_objects.hpp` + and 6 stacked usings in `wallet/remote_node_api.hpp`. These leak into every + consumer TU. +- **Console I/O bypasses the fc logging boundary**, concentrated in + `plugins/snapshot/plugin.cpp` (81 `cerr`/`cout`/`printf` callsites with + bespoke ANSI-color output). Note: a handful of `std::cerr` calls in + `database.cpp` are the deliberate stall-watchdog and must stay. +- **Submodules track no pinned commit discipline.** `.gitmodules` lists three + moving forks with no `branch=`/tag; `submodule update --remote` can silently + advance them. +- **Repo hygiene:** `.qoder/` (168 tracked files, ~6.6 MB of AI-tool scratch) + and a stale `documentation/` tree are still tracked. *(Both addressed in open + PRs #144 and #145.)* -The chain evolution model is hardfork-driven. `libraries/chain/hardfork.d/` holds 13 `.hf` files (0-preamble, 1.hf through 12.hf) that get concatenated at build time into `hardfork.hpp`. There are 126 `has_hardfork(HARDFORK_*)` conditionals in `libraries/chain/`. Most are necessary for historical replay correctness — removing them would break sync from genesis. This is intrinsic complexity, not debt. +## Architectural mental model -The wallet/CLI layer is the most aggressive offender for header pollution: 12+ `using namespace` directives in public headers, including the canonical `using namespace std;` anti-pattern in `wallet.hpp:16`. +VIZ is a Graphene-derived blockchain (Steem/Hive/BitShares lineage) with a +Fair-DPOS consensus tweak, mid-migration to a "VIZ Ledger" DLT positioning +(snapshot-assisted state storage). Four layers: + +1. **`thirdparty/`** — `fc` (serialization, logging, exceptions, async), + `chainbase` (mmap object DB), `appbase` (plugin lifecycle). Vendored as + submodules. +2. **`libraries/`** — `protocol` (operations, types, asset), `chain` (state + machine, evaluators, hardforks), `network` (P2P — note the recent + `node.cpp` → `dlt_p2p_node.cpp` DLT redesign), `api`, `wallet`, `time`, + `utilities` (BIP39 wordlist, key derivation). +3. **`plugins/`** — ~20 appbase plugins registered explicitly in + `programs/vizd/main.cpp` `register_plugins()`. Data plugins index chain + state; API plugins expose JSON-RPC. `mongo_db` has moved out to + `examples-plugins/`. +4. **`programs/`** — `vizd`, `cli_wallet`, `util`, `build_helpers`. + +**Where it breaks down:** the clean layering is undercut by god-file +concentration in `database.cpp` (block apply, fork resolution, undo sessions, +snapshot integration, hardfork application, a push-block stall monitor) and by +`dlt_p2p_node.cpp` / `snapshot/plugin.cpp` each pooling many responsibilities in +one high-churn TU. An in-flight witness→validator rename is visible in the churn +history (`witness.cpp`, `witness_guard.cpp` → `validator*`). ## Findings -| ID | Category | File:Line | Severity | Effort | Description | Recommendation | -|-------|---------------------|---------------------------------------------------------------------|----------|--------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------| -| F001 | Architectural decay | `libraries/chain/database.cpp:1` | High | L | 6,505-LOC god file: block push, fork resolution, undo state, snapshot import, hardfork apply, p2p side-effects. 38 commits in 6 months — highest churn in chain/. | Extract `apply_block_path` (push/pop/apply), `snapshot_integration`, and `undo_session_lifecycle` into separate translation units. Keep `database.hpp` API stable. | -| F002 | Architectural decay | `libraries/network/node.cpp:1` | High | L | 5,759-LOC god file in p2p layer. Peer discovery, message routing, fetch coordination, sync state — all in one TU. 25 commits in 6mo. | Split `node_impl` along the boundaries already implicit in member groupings: `peer_db_node`, `sync_node`, `fetch_node`. Mechanical extraction. | -| F003 | Architectural decay | `plugins/snapshot/plugin.cpp:1` | High | L | 3,277 LOC and **55 commits in 6 months** — highest churn in the whole repo. Single-file plugin handles import, export, P2P transfer, peer query, ANSI logging. | Split into `snapshot_import.cpp` / `snapshot_export.cpp` / `snapshot_peer_query.cpp`. The `detail::set_*` helpers (lines 60-90+) deserve their own header. | -| F004 | Architectural decay | `libraries/wallet/wallet.cpp:1` | Medium | L | 2,886 LOC. Wallet API impl mixes account ops, key management, transaction signing, paid-subscription, social-network forwarding into one class. | Extract `wallet_keys_impl`, `wallet_transaction_impl`, `wallet_account_impl` partial classes. Header (1,568 LOC) similarly bloated. | -| F005 | Architectural decay | `libraries/chain/chain_evaluator.cpp:1` | Medium | M | 2,347-LOC monolith of operation evaluators. New operations are appended; no logical grouping. | Group by operation category (account_*, content_*, witness_*, asset_*) into 4-5 evaluator TUs. Each evaluator is independent — low merge risk. | -| F006 | Architectural decay | `libraries/wallet/include/graphene/wallet/wallet.hpp:1` | Medium | M | 1,568-LOC public header. Includes too much, drags `private_message`, `social_network`, `tags`, `database_api` types into wallet TU. | Forward-declare and move impls to `.cpp`. Reduces compile fan-out for cli_wallet. | -| F007 | Architectural decay | `libraries/protocol/include/graphene/protocol/chain_operations.hpp:1` | Medium | M | 1,188-LOC header with all operation structs. Every consumer pays the include cost for every op type. | Group ops into per-category headers; keep umbrella `chain_operations.hpp` for compatibility. | -| F008 | Architectural decay | `plugins/CMakeLists.txt:2,8` | Low | S | `set(ENV{CHAIN_INTERNAL_PLUGINS} …)` glob-appends every plugin subdir to an env var. **Never read anywhere** in CMake or source. | Delete the env-var lines; keep the glob if `add_subdirectory` is doing useful work. (Tier A candidate.) | -| F009 | Architectural decay | `programs/vizd/main.cpp:72` | High | S | `test_api_plugin` registered unconditionally in production `vizd`. Plugin's API returns hardcoded `"A"`/`"B"` (`plugins/test_api/test_api_plugin.cpp:25-37`). | Wrap registration in `#ifdef BUILD_TESTNET` (or remove the plugin entirely — it has no real consumers). | -| F010 | Architectural decay | `programs/build_helpers/cat-parts.cpp:1` | Low | S | 68-LOC C++ tool used **only on MSVC** (`libraries/chain/CMakeLists.txt:3`). Linux/macOS use `cat_parts.py` (line 8). Compiled into every build regardless. | Either delete cat-parts.cpp and use Python on MSVC too, or delete cat_parts.py and use the C++ binary everywhere. One implementation, one place. | -| F011 | Consistency rot | `plugins/snapshot/plugin.cpp:50-54` | High | M | Bespoke ANSI-color logging macros (`CLOG_GREEN`, `CLOG_ORANGE`, etc.) that pipe to `std::cerr`. ~40 callsites between lines 1100-2500 use this instead of fc::*log.| Replace with fc logging at appropriate levels. fc supports structured fields; cerr-with-ANSI doesn't survive log aggregators. | -| F012 | Consistency rot | `libraries/chain/database.cpp:398,509` | Medium | S | `std::cerr` used directly in chain core (during reindex progress reporting). Same path elsewhere uses `ilog`/`wlog`. | Replace with `ilog("…")`. Keep behavior identical; gain log-config compliance. | -| F013 | Consistency rot | repo-wide | Medium | M | 75 direct console-I/O callsites (`std::cout`, `std::cerr`, `printf`, `fprintf`) across libraries+plugins despite fc-logging being standard. | Sweep with `rg "(std::c(out\|err)\|printf\|fprintf)" libraries/ plugins/`; convert to fc::*log. Most are progress messages or warnings. | -| F014 | Consistency rot | `programs/build_helpers/` | Medium | S | Two implementations of "concatenate `.d` files" (`cat-parts.cpp`, `cat_parts.py`). Drift risk: changes in one not reflected in the other. | Pick one. See F010. | -| F015 | Consistency rot | repo-wide | Low | - | Threading uses both `std::thread`/`std::mutex` (9 callsites) and `fc::thread`/`fc::mutex`/`fc::scoped_lock` (56 callsites). | Document when each is preferred (fc:: integrates with fc::future and the cooperative scheduler; std:: is for one-shot OS threads). Add a short note in CLAUDE.md or developer.md. | -| F016 | Consistency rot | `share/vizd/config/` | Low | S | 7 config templates (`config.ini`, `config_debug.ini`, `config_debug_mongo.ini`, `config_mongo.ini`, `config_stock_exchange.ini`, `config_testnet.ini`, `config_witness.ini`). Unclear which is canonical. | Add `share/vizd/config/README.md` describing what each template is for. Or delete the ones not exercised by Docker images. | -| F017 | Consistency rot | `documentation/building.md:374` | Low | S | Lists `chain_test` as a build target. No such target exists in any `CMakeLists.txt`. | Remove the line. (Will collapse to F033 once test debt is addressed.) | -| F018 | Type & contract | `libraries/wallet/include/graphene/wallet/wallet.hpp:16` | High | S | `using namespace std;` in a public header. Leaks std::* into every TU including this header. | Delete the line. Qualify uses with `std::` (mostly `std::string`, `std::vector`). | -| F019 | Type & contract | `libraries/wallet/include/graphene/wallet/remote_node_api.hpp:28-38` | High | S | 10 `using namespace plugins::*` directives at namespace scope in a public header. Names from condenser_api, database_api, follow, social_network, etc. all leak. | Move usings to .cpp, or replace with `using ns::Type` declarations for only the names actually needed. | -| F020 | Type & contract | `libraries/chain/include/graphene/chain/fork_database.hpp:15` | Medium | S | `using namespace boost::multi_index;` in public header. | Move into .cpp. | -| F021 | Type & contract | `libraries/chain/include/graphene/chain/chain_object_types.hpp:16` | Medium | S | `using namespace boost::multi_index;` in public header. | Move into .cpp. | -| F022 | Type & contract | `libraries/protocol/include/graphene/protocol/operation_util_impl.hpp:9` | Medium | S | `using namespace graphene::protocol;` in a header that gets transitively included by every operation visitor. | Use qualified names. | -| F023 | Type & contract | `libraries/chain/include/graphene/chain/dlt_block_log.hpp:9` | Low | S | `using namespace graphene::protocol;` in header. | Move to .cpp. | -| F024 | Type & contract | `libraries/chain/include/graphene/chain/block_log.hpp:9` | Low | S | Same pattern. | Move to .cpp. | -| F025 | Type & contract | repo-wide (libraries+plugins) | Medium | M | 14 raw `new T(…)` callsites and 24 raw `delete` callsites in `.cpp` files. C++14 has `make_unique`; the project is C++14. | Sweep: replace `new X(...)` paired with `delete` by `std::unique_ptr`/`std::make_unique`. `new char[N]` + `delete[]` → `std::vector` or `make_unique(N)`. | -| F026 | Type & contract | `libraries/network/message_oriented_connection.cpp:262` | Low | S | `std::unique_ptr padded_message(new char[size_with_padding])` — uses raw `new` even though it's wrapped in unique_ptr. | `auto padded_message = std::make_unique(size_with_padding);` | -| F027 | Test debt | repo-wide | Critical | XL | **Zero unit tests.** No `tests/` dir, no inline `BOOST_AUTO_TEST_CASE`. The chain handles money and consensus and has no automated correctness gate. | Start with golden-replay regression tests for the highest-risk paths: block push, fork resolution, snapshot import. Even one harness that replays a known mainnet block range would close the largest correctness gap. | -| F028 | Test debt | `documentation/testing.md` | Low | S | File describes a testing system that doesn't exist. | Either build the system the doc describes, or rewrite the doc to describe testnet-replay-based validation. | -| F029 | Test debt | `documentation/building.md:374` | Low | S | Lists `chain_test` as a `make` target. No such target. | Delete the line until F027 is addressed. | -| F030 | Test debt | `plugins/snapshot/plugin.cpp` (3,277 LOC, 55 commits/6mo) | High | XL | Highest-churn file in the repo. Zero tests. Snapshot import is consensus-adjacent (chain replays from snapshot state). | Property tests for the `detail::set_shared_*` helpers (pure functions, easy to test). Round-trip test: export → import → state-equivalent. | -| F031 | Test debt | `libraries/chain/database.cpp` (6,505 LOC, 38 commits/6mo) | Critical | XL | Highest-churn file in chain core. Zero tests. Behavior changes here are validated only by mainnet/testnet observation. | Same approach as F030 but on smaller leaf functions first. `validate_block_header_signature`, `apply_hardfork_*`, undo-session lifecycle are testable in isolation. | -| F032 | Dependency & config | `.gitmodules` | Medium | S | All three submodules track moving branches (`update`, `lib-boost-1.71`), not pinned commits/tags. Reproducible builds at an old commit can silently shift. | Pin to commit SHAs. The submodule directory entry already records the SHA — but tracking a branch means a fresh `submodule update --remote` would silently advance it. Audit `.gitmodules` and add `update = none` or pin via tag. | -| F033 | Dependency & config | `plugins/mongo_db/` | Medium | S | `ENABLE_MONGO_PLUGIN` (`CMakeLists.txt:71`) gates a 5-file plugin with no CI coverage. `Dockerfile-mongo` exists but no workflow exercises it. | Either add a CI smoke build or mark the plugin deprecated and remove. The risk is silent bit-rot. | -| F034 | Dependency & config | `share/vizd/docker/Dockerfile-lowmem` | Low | S | Lowmem image documented in `building.md:33` but no workflow builds it. Same bit-rot risk as F033. | Add a CI build matrix entry, or consolidate the variant set. | -| F035 | Dependency & config | `CMakeLists.txt:42` | Low | M | `BUILD_SHARED_LIBRARIES` option exists but Docker forces FALSE. Many CMakeLists carry dual `if(BUILD_SHARED_LIBRARIES)` branches (e.g., `libraries/chain/CMakeLists.txt:16-71` vs :73-128) — 50+ lines of duplicated source lists per branch. | If shared builds aren't supported in production, drop the option and the duplicated `add_library(... SHARED)` branches. | -| F036 | Performance | `libraries/chain/database.cpp:1141` | Low | - | TODO: "no easy way to catch boost::interprocess::bad_alloc" — long-standing issue. Not blocking, but a known cliff for shared-mem exhaustion. | Track in an issue. Note it in `documentation/shared-memory.md` if user-visible. | -| F037 | Performance | `libraries/chain/database.cpp:2755` | Low | M | TODO: "this method can be skipped for validation-only nodes" — known optimization gap on the validation hot path. | Add a `skip_*` flag to the apply path; benchmark on a low-mem replay before/after. | -| F038 | Performance | `plugins/snapshot/plugin.cpp:1100-1471` | Low | S | Snapshot import logs progress via `std::cerr` line-by-line; on slow stdouts (e.g. journald with rate limiting) this can throttle a multi-GB import. | Use buffered fc logging or rate-limit progress messages. | -| F039 | Error handling | `libraries/network/node.cpp:4192-4395` | Low | M | 17 `catch(...)` handlers in this range. Most log via fc; pattern is consistent. But a couple (sample audit needed) may continue silently. | Audit each handler: if "log + continue" is the intent, factor into a `LOG_AND_CONTINUE(reason)` helper. Naming makes intent explicit. | -| F040 | Error handling | `libraries/chain/proposal_evaluator.cpp:75` | Medium | S | `} catch (...) {` inside a proposal-validation path with no handler body shown in the snippet. Silent failure is consensus-relevant. | Verify the handler body. If it swallows, replace with explicit fc::exception conversion + propagate. | -| F041 | Error handling | `plugins/chain/plugin.cpp:506,520` | Medium | S | `std::exit(0)` from inside plugin code. Bypasses appbase shutdown sequence. Marked TODO. | Replace with `appbase::app().quit()` per the existing TODO. Behavior delta should be small. | -| F042 | Error handling | repo-wide | Low | - | 23 TODO/FIXME markers in source, several pre-hardfork-2 (`chain_evaluator.cpp:1168` "Remove after hardfork 2"). Hardfork 12 has shipped per `hardfork.d/`. | Sweep: which TODOs reference hardforks ≤ HF12? Those are dead conditionals — delete the gate, keep the post-fork branch. | -| F043 | Security | repo-wide | Low | - | No hardcoded secrets found via simple regex sweep on `password=`, `secret=`, `private_key=`, `api_key=`, `token=` patterns. Passes basic check. | n/a — keep this finding to mark that the check was performed. | -| F044 | Security | `libraries/wallet/include/graphene/wallet/wallet.hpp:16` | Low | S | `using namespace std;` in cli_wallet's transitive include set. Increases ADL surprise surface in JSON-RPC dispatch (where `std::string` and project string types coexist). | Same fix as F018. | -| F045 | Security | `plugins/snapshot/plugin.cpp:2425+` | Medium | M | Snapshot peer-query path downloads multi-GB blobs from "trusted" peers. Trust model is encoded in config (`trusted_snapshot_peers`). Verify signature/hash chain on import path is mandatory and not skippable by config. | Audit the import path: confirm `validating snapshot checksum` (line 1165) covers the entire payload and uses a key derived from chain consensus, not a static cert. | -| F046 | Documentation drift | `documentation/plugin.md:5-9` | Medium | S | References `libraries/plugins`, `external_plugins`, `example_plugins`, and `hello_api`. None exist. The actual plugin layout is `plugins//`. | Rewrite the section to describe the real layout. Either do that, or rebuild the directories the doc promises (former is cheaper). | -| F047 | Documentation drift | `documentation/plugin.md:24` | Low | S | References `programs/util/newplugin.py`. Doesn't exist (`programs/util/` contains only sign_digest, test_block_log, test_shared_mem, sign_transaction, get_dev_key). | Remove the autogeneration section, or add the script back if it's wanted. | -| F048 | Documentation drift | `documentation/plugin.md:5` | Medium | S | Claims `CHAIN_INTERNAL_PLUGINS` is "used to create a runtime-accessible list of available plugins used by the argument parsing." It isn't — see F008. | Delete that claim. Plugins are registered explicitly in `programs/vizd/main.cpp:62-88`. | -| F049 | Documentation drift | `documentation/building.md` | Low | S | `Dockerfile-lowmem` and `Dockerfile-mongo` documented in the table at line 30-34 but no CI workflow builds them — see F033, F034. | Drop them from the doc table, or add CI coverage so the doc reflects reality. | -| F050 | Documentation drift | `README.md` | Low | S | "Pre-populated seed node entries can be found in the config templates under `share/vizd/config/`" — true, but doesn't say which template to use. Combined with F016, users don't know which `config_*.ini` to start from. | Same fix as F016 (a README in `share/vizd/config/`). | -| F051 | Build hygiene | `programs/build_helpers/cat_parts.py:14,16` | Medium | S | `return false` and `return true` (lowercase) — `NameError` in Python. Unreachable today only because `hardfork.d/` happens to contain only regular `.hf` files (the buggy branches require subdirs or empty filter). | Fix to `return False` / `return True`. Two-character change. | - -## Top 5 — if you fix nothing else, fix these - -### 1. **F027 — Add at least one regression test harness** -The combination of (a) zero tests, (b) handling of money / consensus, and (c) high-churn god files (snapshot, database, node) is the largest correctness risk in the repo. You don't need full coverage to materially reduce risk; you need *one* fast harness that replays a known block range and asserts on the resulting state hash. - -**Concrete starting point:** -``` -programs/util/test_block_log.cpp (already exists, deferred per project notes) -``` -Don't delete it — extend it. Add: -- A `replay_to_height` driver that opens block_log, applies blocks up to N, prints final state hash. -- A CI job that runs `replay_to_height 1000000` against a checked-in snapshot + block_log slice. -- A failing test means a consensus-breaking change. Even one such test catches the worst class of bugs. - -This is the kind of test debt that compounds; every week without this is a week where every database.cpp change is rolled out without a safety net. - -### 2. **F009 — Stop registering `test_api_plugin` in production `vizd`** -`programs/vizd/main.cpp:72` currently does: -```cpp -appbase::app().register_plugin(); -``` - -Change to: -```cpp -#ifdef BUILD_TESTNET -appbase::app().register_plugin(); -#endif -``` -Or remove the plugin entirely (its API methods return literal `"A"` / `"B"` strings — no production consumer would want them). One line, no behavioral risk on the production path. - -### 3. **F018 + F019 — Strip `using namespace` from public headers, starting with wallet** -`libraries/wallet/include/graphene/wallet/wallet.hpp:16` has `using namespace std;`. `remote_node_api.hpp:28-38` has 10 more across plugin namespaces. These leak into every consumer of the wallet API. - -**Sketch:** -```cpp -// wallet.hpp:16 — DELETE -- using namespace std; - -// .cpp files that depended on it pick up `using std::string;` etc. -// or qualify call sites. -``` -Mechanical, but careful work — expect compile errors at every unqualified `string`, `vector`, `pair`, `map` callsite in the wallet TU. Run as one PR; reviewers can scan the diff for surprises. - -### 4. **F003 — Split `plugins/snapshot/plugin.cpp` along its natural seams** -3,277 LOC, 55 commits in 6 months — not a refactor candidate, a refactor *necessity*. The file has clean internal sections already: -- import path: lines ~1100-1471 (`load_snapshot_from_*`, `import_state_into_db`) -- export path: separate functions -- peer query path: lines 2425-2530 (`query_trusted_peers_for_snapshot`) -- import detail helpers: namespace `detail` at line 60+ - -**Sketch:** -``` -plugins/snapshot/ - plugin.cpp — entry, init/startup/shutdown, glue - snapshot_import.cpp — load + apply - snapshot_export.cpp — serialize + write - snapshot_peer_query.cpp — P2P peer queries - detail.hpp — set_shared_string, set_buffer, set_shared_authority helpers -``` -No public-API change; pure file-level decomposition. Reviewer cost is low because each callsite stays the same. - -### 5. **F008 + F046 + F048 — Consolidate the `plugin.md` / `CHAIN_INTERNAL_PLUGINS` lie** -Three findings with the same root cause: `documentation/plugin.md` describes a system that doesn't exist (auto-discovered plugins via env var). The CMake glob produces dead state. Pick one of two paths: - -**Path A (cheap, recommended):** Rewrite the doc to describe the actual system (plugins registered in `programs/vizd/main.cpp:62-88`, headers under `plugins//include/graphene/plugins//`, manual `add_subdirectory` in `plugins/CMakeLists.txt`). Delete `set(ENV{CHAIN_INTERNAL_PLUGINS}…)`. Both are 1-PR changes. - -**Path B (expensive):** Build the system the doc describes. Probably not worth it — the existing static registration list is fine. - -## Quick wins - -Low-effort × Medium-or-higher severity. Each takes <30 min and has near-zero risk. - -- [x] **F008** — ~~Delete `set(ENV{CHAIN_INTERNAL_PLUGINS}…)` lines in `plugins/CMakeLists.txt:2,8`.~~ Done (2026-06-17). -- [x] **F009** — ~~Wrap `test_api_plugin` registration in `#ifdef BUILD_TESTNET` or remove (`programs/vizd/main.cpp:72`).~~ Done — no longer registered (2026-06-17). -- [ ] ~~**F012** — Replace `std::cerr` with `ilog`/`wlog` at `libraries/chain/database.cpp:398,509`.~~ **OBSOLETE — do not act.** Target lines gone; remaining `cerr` is the intentional stall-watchdog (see 2026-06-17 refresh banner). -- [x] **F017** — ~~Remove `chain_test` reference from `documentation/building.md:374`.~~ Done (2026-06-17). -- [x] **F018** — ~~Delete `using namespace std;` at `wallet.hpp:15`.~~ Done (2026-06-17): replaced with `using std::{string,vector,map,pair};`. *(Pending CI build verification.)* -- [x] **F029** — ~~Same as F017.~~ Done (2026-06-17). -- [x] **F041** — ~~Replace `std::exit(0)` at `plugins/chain/plugin.cpp:672,698` with `appbase::app().quit()`.~~ Done (2026-06-17). *(Pending CI build verification.)* -- [x] **F046** — ~~Rewrite `documentation/plugin.md` opening section to match real layout.~~ Done (2026-06-17). -- [x] **F047** — ~~Remove the `programs/util/newplugin.py` reference at `documentation/plugin.md:24`.~~ Done (2026-06-17). -- [x] **F048** — ~~Remove the `CHAIN_INTERNAL_PLUGINS` claim from `documentation/plugin.md:5`.~~ Done (2026-06-17). -- [x] **F051** — ~~Fix `false` → `False`, `true` → `True` at `programs/build_helpers/cat_parts.py:14,16`.~~ Done (2026-06-17). +| ID | Category | Location | Sev | Effort | Description | Recommendation | +|------|---------------------|-------------------------------------------------------------|------|--------|-------------|----------------| +| D001 | Test enforcement | `.github/workflows/*`, `CMakeLists.txt:229` | High | M | `tests/consensus_sim/` (32 ASAN/UBSAN test cases across 10 scenario files) exists but `BUILD_CONSENSUS_TESTS` defaults OFF and **no CI workflow builds or runs it**. `docker-pr-build.yml` builds only `vizd`. The correctness gate is written but not enforced. | Add a CI job: `cmake -DBUILD_CONSENSUS_TESTS=ON` + run `consensus_sim_tests` on PRs. This is the highest-leverage change in the repo. | +| D002 | Architectural decay | `libraries/chain/database.cpp` (7,991 LOC, 130 commits/6mo) | High | L | Highest size × churn in the repo. Block apply, fork resolution, undo lifecycle, snapshot import, hardfork apply, stall monitor in one TU. Longest-to-compile file; MSVC needs `/bigobj`. | Extract along functional seams. *(Started in open PR #149 — hardfork cluster → `database_hardfork.cpp`.)* Continue with apply-block path, snapshot integration, undo-session lifecycle. | +| D003 | Architectural decay | `plugins/snapshot/plugin.cpp` (4,657 LOC, 125 commits/6mo) | High | L | Highest-churn plugin. Import, export, P2P transfer, peer query, ANSI logging in one file. Also holds 81 console-I/O callsites (D008) and 43 `catch(...)` handlers. | Split into `snapshot_import` / `snapshot_export` / `snapshot_peer_query` + a detail header. | +| D004 | Architectural decay | `libraries/network/dlt_p2p_node.cpp` (4,380 LOC, 125 commits/6mo) | High | L | God file in the P2P layer (product of the DLT redesign). Peer discovery, routing, fetch coordination, sync state in one TU. | Split `node_impl` along member groupings: peer DB, sync, fetch. Mechanical once the redesign settles. | +| D005 | Architectural decay | `libraries/wallet/wallet.cpp` 2,796 · `wallet.hpp` 1,557 | Med | L | Wallet impl and its 1,557-LOC public header mix account ops, keys, signing, paid-subscription, forwarding. Header drags many plugin types into every cli_wallet TU. | Extract partial-class impls; forward-declare in the header, move bodies to `.cpp`. | +| D006 | Architectural decay | `libraries/chain/chain_evaluator.cpp` (2,350 LOC) | Med | M | Monolith of operation evaluators; new ops appended with no grouping. Low churn (9/6mo) so lower priority than D002–D004. | Group by category (account/content/validator/asset) into separate evaluator TUs. | +| D007 | Type & contract | 51 `using namespace` in public headers | High | M | Leaks into every consumer TU. Worst: `using namespace std;` in `plugins/account_by_key/include/.../account_by_key_objects.hpp:14`; 6 stacked usings in `wallet/include/graphene/wallet/remote_node_api.hpp:19-25`; `using namespace boost::multi_index;` in `chain_object_types.hpp` and `account_by_key_objects.hpp`. | Move usings into `.cpp`, or replace with targeted `using ns::Type;`. Start with `std;` — mechanical, high value. | +| D008 | Consistency rot | `plugins/snapshot/plugin.cpp` (~81), `plugins/chain/plugin.cpp` (11), others | Med | M | ~100 direct `std::cerr`/`cout`/`printf` callsites across libs+plugins despite fc logging being standard; snapshot uses bespoke ANSI-color macros. | Convert to `fc::*log`. **Exception:** the ~5 `std::cerr` calls in `database.cpp` are the intentional stall-watchdog (written to cerr precisely so the monitor never blocks on node locks) — leave them. | +| D009 | Type & contract | 28 raw `new`/`delete` in `database.cpp`, 14 in `chain_evaluator.cpp`, others | Low | M | Project is C++14; `make_unique` available. Consensus-core files, so verify each pairing before converting. | Sweep `new X()`+`delete` → `unique_ptr`; `new char[N]` → `vector`/`make_unique`. Low priority; needs a compiler to verify. | +| D010 | Build config | `CMakeLists.txt` — no default `CMAKE_BUILD_TYPE` | Med | S | Bare `cmake ..` yields empty optimization flags (unoptimized node); `-O3/-O2` set only in the MinGW branch. | *(Open PR #146.)* Default to Release when unset on single-config generators. | +| D011 | Build speed | No `target_precompile_headers` anywhere (min CMake 3.16) | Med | M | Every TU re-parses heavy Boost.MultiIndex / FC reflection headers. | *(Open PR #148 — opt-in `ENABLE_PCH` for chain/protocol/wallet.)* | +| D012 | Build maintenance | 26 `CMakeLists.txt` with dual `SHARED`/`STATIC` source lists | Med | M | Full source list duplicated per library/plugin; already drifted in `libraries/chain` (SHARED branch listed `invite_evaluator.cpp` twice, missing `invite_objects.hpp`). | *(Open PR #147 — single `VIZ_LIBRARY_TYPE` var; also fixes the chain drift.)* | +| D013 | Build hygiene | `programs/build_helpers/` — two hardfork concatenators | Low | S | `cat-parts.cpp` (MSVC) and `cat_parts.py` (everyone else); the C++ binary was compiled on every platform. Drift risk. | *(Open PR #146 gates `cat-parts` to MSVC/MinGW.)* Longer term, pick one implementation. | +| D014 | Dependency & config | `.gitmodules` — 3 submodules, no pinned discipline | Med | S | fc/chainbase/appbase track moving forks with no `branch=`/tag. `submodule update --remote` can silently advance them; reproducible builds from an old commit can shift. | Pin to tags or document that the recorded SHA is authoritative and `--remote` is not to be used. Maintainer decision on which commit is canonical. | +| D015 | Dependency & config | `examples-plugins/mongo_db/` (5 files) | Low | S | Moved out of `plugins/` (good), but no CI smoke build; silent bit-rot risk. | Add a compile-only CI check or mark explicitly unmaintained. | +| D016 | Documentation | `share/vizd/config/` — 5 templates, no README | Low | S | `config.ini`, `config_debug.ini`, `config_stock_exchange.ini`, `config_testnet.ini`, `config_witness.ini` with no guide to which to use; README points users here without saying which. | Add `share/vizd/config/README.md` with a one-line purpose per template. | +| D017 | Repo hygiene | `.qoder/` (168 files, ~6.6MB) + `documentation/` (8 files) | Low | S | AI-tool scratch dir and a legacy doc tree still tracked; the `documentation/` tree overlaps the current `docs/` VitePress site and has drifted. | *(Open PRs #144 untrack `.qoder/`; #145 consolidates `documentation/` into `docs/`.)* | +| D018 | Consistency rot | Root build scripts: `build_mingv.sh` (typo), mixed dash/underscore naming | Low | S | `build_mingv.sh` is misspelled; `documentation/building.md` tells users to run `build_mingw.sh` which doesn't exist. | *(Open PR #144 renames to `build_mingw.sh`.)* | +| D019 | Dead conditional | `chain_evaluator.cpp:1168` "TODO: Remove after hardfork 2" | Low | S | 19 TODO/FIXME markers total; this one gates pre-HF4 behavior and is long past. | Verify replay-safety, then delete the gate and keep the post-fork branch. Treat all other hardfork conditionals as append-only (D-note below). | + +## Top 5 — if you fix nothing else + +1. **D001 — Wire `consensus_sim` into CI.** The tests are already written (ASAN + + UBSAN, deterministic multi-node). Turning them from "exists" into "runs on + every PR" is the biggest correctness win available and needs no new test + code — just a workflow job with `-DBUILD_CONSENSUS_TESTS=ON`. +2. **D002 — Keep splitting `database.cpp`.** 130 commits in six months into one + 7,991-LOC file with no per-file test isolation. PR #149 starts it; continue + seam by seam. +3. **D007 — Strip `using namespace` from public headers**, starting with the + `std;` in `account_by_key_objects.hpp`. Mechanical, high blast-radius win. +4. **D010 + D011 + D012 — Land the build PRs (#146/#147/#148).** Default-Release, + collapsed CMake, and opt-in PCH together cut both wall-clock build time and + the drift surface, at near-zero risk. +5. **D003 / D004 — Split the two other high-churn god files.** Snapshot and + dlt_p2p_node each see ~125 commits/6mo; both are prime bug territory. ## Things that look bad but are actually fine -This section exists because half the trap of doing an audit on a Graphene-derived blockchain is over-flagging consensus-critical code. - -- **`catch(...)` handlers in `libraries/network/peer_connection.cpp:118,130,142,290` and `libraries/network/message_oriented_connection.cpp:222,298`.** These are inside an event loop where any uncaught exception would tear down a peer connection thread. They all log via `dlog`/`wlog`/`elog` and rethrow or set a flag for the connection-closed handler. This is the correct pattern for a fc::thread-driven network layer. Don't "clean up" by removing them. -- **126 `has_hardfork(HARDFORK_*)` checks in `libraries/chain/`.** These are not "stale migration code." They are required for replay correctness — a node syncing from genesis must reproduce the exact behavior at every height. A hardfork conditional from HF1 still matters because someone, somewhere, is replaying a node from block 1. Only the very oldest pre-genesis-launch branches (e.g., `chain_evaluator.cpp:1168` "Remove after hardfork 2") can be cleaned, and even then very carefully. Treat hardfork code as append-only. -- **`programs/util/sign_digest.cpp`, `test_block_log.cpp`, `test_shared_mem.cpp`.** These look like dead dev utilities, but per the project's own deferred-cleanup note, they have plausible operator value. `sign_digest` lets witnesses sign things offline; `test_block_log` is the closest thing to a regression-test driver in the repo (and is the natural home for F027). Don't delete in a Tier B sweep — promote them. -- **`libraries/utilities/words.cpp` at 49,787 lines.** This is a BIP39 wordlist embedded as a C++ array. The size is intrinsic to the data, not a code-quality issue. Don't add it to "god file" findings. -- **Boost coroutine being found separately from other Boost components** (`CMakeLists.txt:85-89`). Looks like duplication, but this is a known workaround for a Boost CMake config quirk where `coroutine` isn't pulled in by `find_package(Boost ... COMPONENTS thread …)` cleanly across all distributions. -- **Mixed `std::thread` and `fc::thread`.** `fc::thread` is required for cooperative scheduling with `fc::future`/`fc::async` and the message-bus pattern (see VERIFY_CORRECT_THREAD usage in network code). `std::thread` is correct for one-shot OS threads that don't interact with fc. The mix is intentional, not rot. (F015 is a doc finding, not a code finding.) -- **Two CMake branches per library — `BUILD_SHARED_LIBRARIES` ON vs OFF.** Looks like 2× maintenance, but the duplication is in source-list arrays; the `add_library(SHARED)` vs `add_library(STATIC)` is a real and load-bearing distinction for some downstream consumers. Removal (F035) is a judgment call on whether shared-lib builds are still supported. -- **The 6,505-line `database.cpp` is a god file but is not "rotten."** It's huge because the chain state machine has lots of facets — block apply, undo session lifecycle, snapshot integration, hardfork application — and they share a lot of `_db.session()` / object-index access. Splitting (F001) is good debt reduction, but this isn't the same as a 6,500-line god file in a typical web service. Don't recommend a rewrite. Recommend extraction along functional seams. +- **`libraries/utilities/words.cpp` (49,787 lines).** BIP39 wordlist as a + `const char* word_list[]` in `.rodata`. Size is intrinsic to the data — not a + god file. +- **132 `has_hardfork(HARDFORK_*)` checks in `libraries/chain/`.** Required for + replay correctness — a node syncing from genesis must reproduce behavior at + every height. Treat as append-only; only the very oldest pre-launch gates + (D019) can be removed, and only carefully. +- **`catch(...)` in `peer_connection.cpp` / `message_oriented_connection.cpp` + and the validator/p2p event loops.** These sit in fc::thread-driven loops + where an uncaught exception would tear down a connection thread; they log via + `dlog`/`wlog`/`elog` and set a flag. Correct pattern — do not "clean up." +- **The ~5 `std::cerr` calls in `database.cpp`.** Deliberate stall-watchdog + output, written to cerr so the monitor can never block on node locks. + Converting them to fc logging would defeat the watchdog. +- **Mixed `std::thread`/`fc::thread`.** Intentional: `fc::thread` integrates with + `fc::future` and the cooperative scheduler; `std::thread` is for one-shot OS + threads. +- **Boost coroutine found separately in root CMake.** Known workaround for a + Boost CMake config quirk, not duplication. ## Open questions for the maintainer -These are things I couldn't classify as definitely-debt vs. intentional without domain context. - -1. **`test_api_plugin` registration in production vizd (F009).** Is there a downstream tool that probes for this plugin's presence as a node-version sentinel? If yes, removing it is breaking. If no (likely), the fix is a one-liner. -2. **Snapshot peer-trust model (F045).** Is the snapshot checksum (`plugin.cpp:1165, 1172`) a content hash, or is it tied to chain state at the snapshot height? The difference matters for whether a malicious "trusted_snapshot_peer" can serve a forged-but-consistent snapshot. -3. **`BUILD_SHARED_LIBRARIES` (F035).** Are there real-world consumers of the shared-lib build, or is it dead-but-unproven? If the latter, F035 becomes a quick-win deletion of half of every library's CMakeLists. -4. **Mongo plugin status (F033).** Is mongo_db plugin still a supported deployment target, or is it kept around for one specific operator? If the latter, capture that in a comment and CI; if the former, it's actively bit-rotting without coverage. -5. **Submodule branches vs commits (F032).** Is the team intentionally tracking `update`/`lib-boost-1.71` branches so that `submodule update --remote` follows them, or is the branch entry vestigial and the SHA pin is what's authoritative? Different fixes apply. -6. **`programs/util/` operator value.** Per the project notes, `sign_digest`, `test_block_log`, `test_shared_mem` are deferred for operator audit. Useful for F27 (regression test starter). Are operators actively using them today, or has tooling moved on? -7. **`config_stock_exchange.ini`.** Why does this exist as a top-level config template in `share/vizd/config/`? Either it documents a real deployment shape (capture in a README) or it's vestigial. +1. **D001 CI cost.** `consensus_sim` builds at `-O1 -g -fsanitize=address,undefined`. + Is the CI runner budget OK with an ASAN build per PR, or should it run on a + schedule/label instead of every PR? +2. **D014 submodules.** Is tracking moving forks intentional (so `--remote` + follows them), or is the recorded SHA authoritative? Different fixes apply. +3. **D015 mongo_db.** Is the `examples-plugins/mongo_db` plugin a supported + deployment target or a reference example? Determines whether it needs CI. +4. **D005 wallet header.** Is the cli_wallet compile-time fan-out from + `wallet.hpp` a felt pain, or tolerable? Drives priority of the header split. +5. **`config_stock_exchange.ini`.** Does this document a real deployment shape + worth keeping, or is it vestigial? (Feeds D016.) diff --git a/docs/advanced/hardfork-management.md b/docs/advanced/hardfork-management.md index e4fffae7ec..f1a00b3dee 100644 --- a/docs/advanced/hardfork-management.md +++ b/docs/advanced/hardfork-management.md @@ -48,6 +48,8 @@ When the head block time passes `next_hardfork_time` and sufficient validators s | 10 | Inflation model | | 11 | Emission model changes | | 12 | Emergency consensus recovery (see below) | +| 13 | Distribution epoch length (`chain_properties_hf13`) | +| 14 | Prediction Markets (Onix): 18 ops + 7 virtual ops, CPMM/LMSR pricing, parimutuel settlement, oracles, disputes, commit-reveal, lazy pool; chain properties v5 | --- diff --git a/docs/consensus/emergency-consensus.md b/docs/consensus/emergency-consensus.md index 74d7d0092d..91d99ee370 100644 --- a/docs/consensus/emergency-consensus.md +++ b/docs/consensus/emergency-consensus.md @@ -151,9 +151,9 @@ Several P2P safeguards are emergency-aware: | Guard | Behavior during emergency | |-------|--------------------------| | `resync_from_lib()` | **Skipped entirely** — popping blocks near LIB during emergency would crash | -| `stale_sync_check_task()` | If master's head is advancing → reset timer, skip recovery; if follower head is stuck → allow recovery | +| `check_wedge_watchdog()` | **No emergency carve-out.** It arms only when the head is frozen far below a *corroborated* network tip (second-highest head across established peers), and any head advance resets it. Log-only unless `auto-resync-on-wedge = true`, in which case a confirmed wedge exits the node | | `handle_block()` (DLT, sync mode, gap 0–2) | Treated as normal (not sync) to prevent production loop disruption | -| Snapshot stalled sync detection | Same logic as stale sync check | +| Snapshot stalled sync detection (`enable-stalled-sync-detection`, off by default) | **This is where the master/follower carve-out actually lives.** On timeout it reads `emergency_consensus_active`; if we are the emergency master (holds the emergency key and the committee is in the schedule) solo production is normal, so recovery is skipped. A stuck follower is allowed to recover | The `resync_from_lib()` guard is the most critical: during emergency, LIB is close to HEAD. Popping blocks back to LIB and resetting the fork DB would cause peer blocks from the real network to link to the re-seeded LIB, trigger a fork switch, pop below the committed LIB, and either crash or corrupt state. @@ -196,7 +196,7 @@ Snapshots created during an active emergency preserve the state correctly; snaps | 6 | `maybe_produce_block` (master) | Bypass sync, stale, participation; skip minority fork | | 7 | `maybe_produce_block` (follower) | Must sync first; 21-block isolation check | | 8 | `resync_from_lib` | **Skip entirely** during emergency | -| 9 | `stale_sync_check_task` | Skip if master's head advancing; allow if follower stuck | +| 9 | `check_wedge_watchdog` | No emergency-specific branch; corroboration + head-advance resets are what keep it from arming | | 10 | `handle_block` | Near-caught-up blocks treated as normal in DLT emergency | | 11 | `database::open` | Startup schedule repair | | 12 | `validator_guard` | Do not suppress key restoration during emergency | diff --git a/docs/governance/chain-properties.md b/docs/governance/chain-properties.md index 52ae9beb4e..5517dfa254 100644 --- a/docs/governance/chain-properties.md +++ b/docs/governance/chain-properties.md @@ -122,8 +122,28 @@ Properties were introduced in hardfork stages: | `chain_properties_hf4` | 1 | HF4 | inflation_validator_percent, inflation_ratio_committee_vs_reward_fund, inflation_recalc_period | | `chain_properties_hf6` | 2 | HF6 | data_operations_cost_additional_bandwidth, validator_miss_penalty_percent, validator_miss_penalty_duration | | `chain_properties_hf9` | 3 | HF9 | create_invite_min_balance, committee_create_request_fee, create_paid_subscription_fee, account_on_sale_fee, subaccount_on_sale_fee, validator_declaration_fee, withdraw_intervals | +| `chain_properties_hf13` | 4 | HF13 | distribution_epoch_length | +| `chain_properties_pm` | 5 | HF14 | ~30 prediction-market parameters + kill-switches `pm_commit_reveal_enabled`, `pm_lazy_pool_enabled` | -Use version index 3 (`chain_properties_hf9`) for all new validator property submissions. +Use version index **5** (`chain_properties_pm`) for all new validator property submissions. (Index 4 is `chain_properties_hf13`, which added `distribution_epoch_length`.) + +### Prediction-market parameters (v5, HF14) {#pm-parameters} + +All median-voted; see [Prediction Market Operations](../protocol/operations/prediction-markets.md). + +All PM percentages are **bp (10000 = 100.00%)**, like the other `*_percent` properties — no permille (‰) anywhere. + +- **Oracle:** `pm_min_oracle_insurance`, `pm_max_oracle_fee_percent` (the *only* governed fee cap — on the oracle %), `pm_oracle_registration_fee`, `pm_oracle_penalty_percent`, `pm_oracle_dispute_response_sec`, `pm_oracle_accept_window_sec` (default 3600 = 1h — the named oracle must accept or reject a pending market within this window; on expiry the cron refunds the creator's seed liquidity, but **not** the creation fee, and voids the market → `pm_market_expired`). +- **Risk / coverage** *(percent of a market's betting volume, 100 = 1.0×):* `pm_listing_min_coverage_percent` (250 = 2.5×) — markets whose oracle insurance covers less than this share of their volume are hidden from the default `list_markets` catalog (revealed via `show_risky`); `pm_betting_min_coverage_percent` (150 = 1.5×) — advisory threshold, published for clients to require an explicit risk confirmation before betting (not enforced on-chain; must be `≤ pm_listing_min_coverage_percent`). +- **Market:** `pm_min_liquidity`, `pm_market_creation_fee`, `pm_max_outcomes`, `pm_max_market_duration`. *(There is no aggregate fee cap; creator/liquidity fees are uncapped and self-limiting, with a static `sum ≤ 100%` solvency bound.)* +- **Batch / commit-reveal:** `pm_batch_epoch_blocks`, `pm_reveal_window_blocks`, `pm_min_batch_bet`, `pm_commit_no_reveal_penalty_percent`, `pm_commit_reveal_enabled`. +- **Disputes:** `pm_dispute_fee`, `pm_dispute_grace_sec`, `pm_dispute_vote_period_sec`, `pm_dispute_auto_close_sec`, `pm_dispute_approve_min_percent`, `pm_no_contest_penalty_percent`, `pm_dispute_reward_multiplier` (bp multiplier, 10000 = 1×). +- **Time penalty:** `pm_default_time_penalty_percent`, `pm_max_time_penalty`. +- **Lazy pool:** `pm_lazy_pool_enabled`, `pm_lazy_alloc_percent`, `pm_lazy_max_total_alloc_percent`, `pm_lazy_recall_step_percent`, `pm_lazy_lock_sec`, `pm_lazy_emergency_penalty_percent`, `pm_lazy_min_liquidity_fee_percent` (default 200 = 2% — the pool refuses to co-provide liquidity to a market whose `liquidity_fee_percent` is below this reward floor). +- **Leverage (opt-in):** `pm_leverage_enabled`, `pm_leverage_fund_percent`, `pm_leverage_max_per_position_bp`, `pm_leverage_max_position_ratio_percent`, `pm_leverage_min_market_liquidity`, `pm_leverage_safety_margin_percent`, `pm_leverage_max_slippage_percent`, `pm_leverage_m_factor_percent`, `pm_leverage_pool_profit_percent`, `pm_leverage_expiration_buffer_sec`, `pm_conversion_profit_cost_percent`. +- **Fairness:** `pm_processing_cap_per_block`. + +The three `*_enabled` flags (`pm_commit_reveal_enabled`, `pm_lazy_pool_enabled`, `pm_leverage_enabled`) are live kill-switches: the validator median can disable commit-reveal, the lazy pool, or leverage without a new hardfork. --- diff --git a/docs/node/configuration.md b/docs/node/configuration.md index 2121d967b8..7a892d090e 100644 --- a/docs/node/configuration.md +++ b/docs/node/configuration.md @@ -158,6 +158,11 @@ Leave these unset on non-validator nodes. # Allow production even if chain is stale (development/testnet only) enable-stale-production = false +# Disable minority-fork detection (single-operator testnet/fork ONLY). +# Unlike enable-stale-production, this is never auto-cleared by healthy +# participation. Never enable on a real public network. +disable-minority-fork-detection = false + # Minimum participation % required to produce blocks (0–99) required-participation = 33 @@ -203,4 +208,4 @@ All options listed by source file: | `plugins/chain/plugin.hpp` | `shared-file-size`, `min-free-shared-file-size`, `inc-shared-file-size`, `block-num-check-free-size`, `single-write-thread`, `enable-plugins-on-push-transaction`, `read-wait-micro`, `max-read-wait-retries`, `write-wait-micro`, `max-write-wait-retries`, `skip-virtual-ops`, `clear-votes-before-block`, `track-account-range`, `history-whitelist-ops`, `history-blacklist-ops`, `history-start-block` | | `plugins/p2p/p2p_plugin.hpp` | `p2p-endpoint`, `p2p-max-connections`, `p2p-seed-node`, `checkpoint` | | `plugins/webserver/webserver_plugin.hpp` | `webserver-http-endpoint`, `webserver-ws-endpoint`, `webserver-thread-pool-size` | -| `plugins/validator/validator.hpp` | `enable-stale-production`, `required-participation`, `validator`, `private-key`, `emergency-private-key`, `fork-collision-timeout-blocks`, `ntp-server`, `ntp-request-interval`, `debug-block-production` | +| `plugins/validator/validator.hpp` | `enable-stale-production`, `disable-minority-fork-detection`, `required-participation`, `validator`, `private-key`, `emergency-private-key`, `fork-collision-timeout-blocks`, `ntp-server`, `ntp-request-interval`, `debug-block-production` | diff --git a/docs/node/docker.md b/docs/node/docker.md index 2141e4c980..d776ca166b 100644 --- a/docs/node/docker.md +++ b/docs/node/docker.md @@ -110,6 +110,35 @@ docker run -d \ vizblockchain/vizd:testnet ``` +### Testnet bootstrap + +When a node starts without seed nodes, it tries to open a `snapshot.json` +file holding the initial users and their balances. If found, liquid tokens are +transferred from the `CHAIN_INITIATOR_NAME` account to user accounts as vesting +(shares). + +Initial users and their keys are defined in +`libraries/protocol/include/graphene/protocol/config.hpp`: + +- `CHAIN_INITIATOR_NAME` (`viz`) — holds the initial balance. +- `CHAIN_COMMITTEE_ACCOUNT` — the initial active validator. + +`viz` keys: + +- private: `5JabcrvaLnBTCkCVFX5r4rmeGGfuJuVp4NAKRNLTey6pxhRQmf4` +- public: `VIZ6MyX5QiXAXRZk7SYCiqpi6Mtm8UbHWDFSV8HPpt7FJyahCnc2T` + +`committee` keys: + +- private: `5Hw9YPABaFxa2LooiANLrhUK5TPryy8f7v9Y1rk923PuYqbYdfC` +- public: `VIZ6Yt7d6LsngBoXQr47aLv97bJVs7jyr7esZTM4UUSpLUf3nbRKS` + +To ease testing, several additional users are immediately available: `alice`, +`bob`, `charlie`, `chuck`, `dan`, `frank`. They all share the same keys: + +- private: `5J9DBCRX5D2ZUUuy9qV2ef9p5sfA3ydHsDs2G531bob7wbEigDJ` +- public: `VIZ5zTAE2hiGcqYaDTQeEBqTtPeoWtSNjpznwmbvqJXesrK1Qn3e8` + --- ## Building Images Locally diff --git a/docs/node/validator-node.md b/docs/node/validator-node.md index 6a00456b04..6aab9535e6 100644 --- a/docs/node/validator-node.md +++ b/docs/node/validator-node.md @@ -164,6 +164,9 @@ If fewer than 33% of validators are participating, production stops to prevent s ### Minority Fork Detection If the node's fork database shows 21+ consecutive blocks all from this node's own validators, it automatically rolls back to LIB and resyncs. This catches network isolation. +> [!WARNING] Single-operator forks +> On a testnet or mainnet fork where **one operator controls all validators**, "21 blocks all ours" is the normal healthy state, so this detector loops forever resetting to LIB. `enable-stale-production = true` does **not** help here: healthy participation (≥33%) auto-clears that override every block. Use `disable-minority-fork-detection = true` instead — it bypasses both the standard and DLT detection paths and is never auto-cleared. **Never enable it on a real public network** — it removes the isolation guard. + ### Production Watchdog If no block has been produced for 180 seconds (60s for emergency master) while `should_be_producing` is true, the watchdog automatically clears stuck flags (`minority_fork_recovering`, P2P catchup, chain syncing) and attempts to resume. diff --git a/docs/plugins/prediction-market-api.md b/docs/plugins/prediction-market-api.md new file mode 100644 index 0000000000..8ebb13d2ce --- /dev/null +++ b/docs/plugins/prediction-market-api.md @@ -0,0 +1,291 @@ +# `prediction_market_api` Plugin + +Read-only JSON-RPC access to HF14 prediction-market state (markets, bets, oracles, liquidity, disputes, the lazy pool, and the v5 chain properties). The plugin returns the raw consensus `pm_*` objects directly, plus a few computed DTOs for values that are derived rather than stored. + +**Enable:** add `prediction_market_api` to the node's plugin list (registered in `vizd` by default). Depends on `chain` + `json_rpc`. + +All list methods page with `from` (skip count) and `limit` (`≤ 1000`). + +--- + +## Methods + +### Markets + +| Method | Args | Returns | +|--------|------|---------| +| `get_market` | `market_id` | `pm_market_object` | +| `list_markets` | `status, from, limit, [show_risky]` | `pm_market_object[]` | +| `list_markets_by_oracle` | `oracle, from, limit` | `pm_market_object[]` | +| `list_markets_by_oracle_status` | `oracle, status, from, limit` | `pm_market_object[]` | +| `list_markets_awaiting_resolution` | `oracle, from, limit` | `pm_market_object[]` | +| `list_markets_by_creator` | `creator, from, limit` | `pm_market_object[]` | +| `get_market_outcomes` | `market_id` | `pm_outcome_object[]` | +| `get_market_weight_sums` | `market_id` | `pm_market_weight_sums` (computed) | +| `get_market_bets` | `market_id, from, limit` | `pm_bet_object[]` | +| `get_market_liquidity` | `market_id, from, limit` | `pm_liquidity_object[]` | +| `get_market_full` | `market_id, [account]` | `pm_market_full` (computed) | + +`status` for `list_markets`: `-1` deleted, `0` waiting, `1` active, `2` closed, `3` resolved. + +`list_markets_by_oracle_status` returns **one oracle's markets filtered to a single `status`** (`1` active, `3` resolved, …) — walked over the dedicated `by_oracle_status` composite index (keyed `oracle, status, id`) as a bounded prefix, instead of pulling the oracle's whole set with `list_markets_by_oracle` and filtering client-side. Use it for an oracle profile's "Active / Resolved" tabs, or to page just an oracle's resolved history. (For markets *awaiting* this oracle's result — active but past betting-close — use `list_markets_awaiting_resolution`, which cannot be expressed by `status` alone.) + +`list_markets_awaiting_resolution` returns the markets that need **this oracle's result now**: active (`status 1`) markets whose betting window has already closed (`betting_expiration ≤ head_block_time`) and are therefore not yet resolved. "Awaiting" is not a distinct status — a market stays `active` from open through betting-close until it is resolved — so it cannot be isolated by `status` alone. This method walks the `by_betting_expiration` index (keyed `status, betting_expiration, id`) over just the bounded prefix of active markets whose betting has passed, avoiding a full scan of the oracle's (mostly resolved) history that an Oracle Console would otherwise have to do client-side. + +`get_market_full` is a **one-call enriched view** for a market-detail screen: it returns the market + outcomes + weight sums + oracle (with reliability) + parsed metadata, and — when the optional `account` is given — that account's bets, leverage positions and LP **on this market**. Saves the thin client several round-trips. + +**Risk listing filter** (security-threat-model §4.3): by default `list_markets` hides under-insured markets — those whose oracle insurance covers less than **2.5×** the market's betting volume. Pass `show_risky = true` to reveal them. Markets are only hidden, never blocked/deleted (betting is always permitted on-chain — a consensus bet-block would be a censorship vector). The lazy-pool exposure penalties (active-market 5% recursive + fault stamps) are enforced in consensus on allocation. + +### Market metadata (off-chain parsed) + +Each market carries a free-form, consensus-opaque `metadata` JSON string. This plugin parses the keys it +indexes (category / subcategory / tags / banned jurisdictions / title / image / condition_id / +**description**) into a `pm_market_meta_object` for discovery, jurisdiction filtering and display — +**display/indexing only, never consensus**. `description` holds the short resolution rules (how the oracle +will resolve the market — surface-level, for clients); the market's on-chain `url` still points to the full +legal terms at the source. + +| Method | Args | Returns | +|--------|------|---------| +| `get_market_meta` | `market_id` | `pm_market_meta_object` (or error if none) | +| `list_markets_by_category` | `category, from, limit, [jurisdiction], [subcategory], [tag], [sort]` | `pm_market_meta_object[]` | +| `get_market_categories` | — | `pm_market_categories` (computed) | + +`list_markets_by_category` excludes markets whose `banned_jurisdictions` contains the optional +`jurisdiction` ISO code — so a regulated client passes its own jurisdiction to get only the markets it +may list. Optional `subcategory` (exact) and `tag` (CSV membership) narrow the set; `sort` ∈ `newest` +(market id desc, default) · `oldest` · `volume` (`bets_sum` desc) · `expiration` (`betting_expiration` +asc). `get_market_categories` returns the live taxonomy — per-category / per-subcategory counts plus the +top 20 hot tags (jurisdiction-* excluded) — aggregated over currently indexed markets, so a browse UI can +build its filter chips without hard-coding a taxonomy. The meta object: +``` +{ market: pm_object_id, + category, subcategory, tags, // strings; tags comma-joined + banned_jurisdictions, // comma-joined ISO codes; empty = allowed everywhere + title, image, condition_id, // display + source back-link + description, // short resolution rules (url = full legal terms) + expiry } // pruned after the dispute window closes + TTL +``` + +### Positions & oracles + +| Method | Args | Returns | +|--------|------|---------| +| `get_account_positions` | `account, from, limit` | `pm_position[]` (bet + `expected_payout`) | +| `get_account_leverage_positions` | `account, from, limit` | `pm_leverage_position_object[]` | +| `get_market_leverage_positions` | `market_id, from, limit` | `pm_leverage_position_object[]` | +| `get_creator_ban` | `account` | `pm_creator_ban_object` (or error if none) | +| `get_oracle` | `owner` | `pm_oracle` (object + `reliability_score` + workload/latency reads) | +| `list_oracles` | `from, limit` | `pm_oracle_object[]` | +| `list_markets_in_dispute_window` | `oracle, from, limit` | `pm_market_card[]` | +| `list_oracle_disputes` | `oracle, from, limit` | `pm_oracle_dispute[]` (computed) | + +`list_markets_in_dispute_window` drills into the `markets_in_dispute_window` gauge: this oracle's resolved (`status 3`, `payout_status 1`) markets that still carry **no dispute row** — i.e. the ones a challenger could still dispute within `pm_dispute_grace_sec` of the announcement. `list_oracle_disputes` drills into the two dispute gauges: this oracle's currently **open** disputes (`status 0`), each tagged with a `stage` — `awaiting_response` (the oracle has not answered yet) or `awaiting_decision` (answered, now with the resolver/committee) — derived from `oracle_response_time`, plus a `market_card` for rendering. Both walk only the oracle's own set (`by_oracle_status` / `by_auto_close`), so they stay O(this oracle), not a global scan. They pair with the stored gauges on `get_oracle` (below): the gauge is the count, these methods are the list. + +### Leverage previews (Boost) + +Read-only quotes that call the **same in-node margin math** the evaluators use, so a preview matches what the corresponding `pm_leverage_*` op would compute at the head block. They are non-consensus estimates (reserves move between the read and the broadcast — always send the on-chain slippage guards). + +| Method | Args | Returns | +|--------|------|---------| +| `get_leverage_quote` | `market_id, outcome_index, collateral` | `pm_leverage_quote` (computed) | +| `get_leverage_close_preview` | `position_id` | `pm_leverage_close_preview` (computed) | +| `get_leverage_convert_preview` | `position_id` | `pm_leverage_convert_preview` (computed) | + +`get_leverage_quote` mirrors `pm_leverage_open`: it returns the max solvent loan and resulting max leverage, the pool/position caps, up to 12 slider stops (each with tokens, threshold, current & worst-case cancel value), and — when leverage is not possible — `available = false` with a `failed_constraints[]` list. `get_leverage_close_preview` / `get_leverage_convert_preview` mirror `pm_leverage_close` / `pm_leverage_convert` at the current reserves (cancel value, pool obligation, what the bettor receives, whether it is closeable/convertible, and the conversion fee at the current median `pm_conversion_profit_cost_percent`). + +> Per-bettor settlement is emitted as the `pm_payout` virtual op (stake, side/outcome, realized payout — +> `0` on a loss); a leveraged position's settlement is the `pm_leverage_resolve` virtual op (with +> `outcome_index`, `won`, `leverage`). Both appear in `account_history`; the leverage-position objects +> themselves are queryable via the two methods above. + +### Disputes, lazy pool, governance + +| Method | Args | Returns | +|--------|------|---------| +| `get_dispute` | `market_id` | `pm_dispute_object` | +| `get_dispute_votes` | `market_id` | `pm_dispute_votes` (votes + live tally) | +| `get_lazy_pool` | — | `pm_lazy_pool_object` | +| `get_lazy_deposit` | `account` | `pm_lazy_deposit_object` | +| `get_lazy_allocations` | `from, limit` | `pm_lazy_allocation_object[]` | +| `get_market_lazy_allocation` | `market_id` | `pm_lazy_allocation_object` (or error if none) | +| `get_pm_chain_properties` | — | `chain_properties_pm` (median, v5) | + +`get_lazy_allocations` lists the lazy pool's per-market allocation records (for a pool dashboard); `get_market_lazy_allocation` fetches the one for a given market. Oracle penalty stamps need no separate method — they ship on `pm_oracle_object` (`penalty_stamps`, `last_penalty_stamp_time`) via `get_oracle`. + +### Charts — kline / weight history + +A time series for plotting how each outcome's weight evolves. The plugin appends one point **every time a market's per-outcome weights change** — a bet, a cancel, a liquidation, a batch settle, a leverage open, or a leverage settlement — as a timestamped snapshot of the parimutuel weight (staked amount) on every outcome. This is **non-consensus** plugin state (kept in chainbase, undo/redo-safe, never part of the state hash); history accrues from the moment the plugin is first enabled on the node. + +**Retention:** the kline history is pruned **together with the market's metadata**, on the same schedule — `result_expiration` + dispute grace + `pmm-ttl-days` (default 7). So a market's full chart is available throughout its life and for the retention window after settlement, then both indexes are cleaned up (draining over several blocks for very long histories) to keep node storage bounded. + +| Method | Args | Returns | +|--------|------|---------| +| `get_market_kline` | `market_id, [from], [limit]` | `pm_kline[]` (ascending by `seq`) | + +Pagination is **offset-from-newest** (kept deliberately simple for thin clients): `from` is how many of the **newest** points to skip, `limit ≤ 1000` is the page size. +- `(market_id, 0, 1000)` → the latest ≤ 1000 changes. +- `(market_id, 1000, 1000)` → the previous 1000 (one page further back) — repeat with `from += 1000` to lazy-load older history. + +Plot it as: x = `timestamp` (unix seconds), and one line per outcome `i` with y = `weights[i]` (or normalized `weights[i] / Σweights` for the implied probability). + +--- + +## Computed DTOs + +These wrap raw objects with values derived at read time (non-consensus). + +**`pm_position`** — a bet plus its parimutuel payout: +``` +{ bet: pm_bet_object, + expected_payout: share_type, // payout if this side wins (or realized once settled) + market_status: int8, + resolved_outcome: int16 } +``` +`expected_payout` byte-mirrors `settle_market`: for an active bet it is the conditional payout if the chosen side wins (`amount + winners_pool × weight / Σweight − time_penalty`); once settled it is the realized `resolved_amount`. + +**`pm_oracle`** — the raw `pm_oracle_object` plus read-time computed fields. All non-consensus (display only; never gate consensus). + +- `reliability_score` — bp `[0..10000]`. **v2.1** blends four reputation ratios, then docks decayed penalty stamps and bans, then confidence-shrinks a thin track record toward a neutral prior: + ``` + accuracy = markets_resolved / (markets_resolved + missed_count) // resolved vs missed-deadline + verdicts = disputes_won / (disputes_won + disputes_lost) // dispute outcomes + responsive = (disputes_received − dispute_responses_missed) / disputes_received + timely = (markets_resolved − resolved_late_count) / markets_resolved // on-time vs past-deadline resolves + score = accuracy·40% + verdicts·30% + responsive·15% + timely·15% + score −= penalty_stamps × 300bp (halved per 10 days since last_penalty_stamp_time) + score −= bans_received × 1500bp + if markets_resolved < 20: shrink toward a 6000 prior // unproven oracles neither sit at 100 nor crater + ``` + Each ratio defaults to a full `10000` until the oracle has the relevant history (optimistic when unproven). `timely` is the P5 lateness signal: a late-but-delivered resolve still counts as `markets_resolved` (so it earns full `accuracy`), and `timely` is what separates a chronically-late oracle from a punctual one. `avg_resolution_time` is deliberately **not** scored — it measures latency from betting close, not deadline overrun, and can't be normalized without the market length. Weights are non-consensus; tune freely. +- **Workload gauges** (stored on `pm_oracle_object`, O(1)-maintained across the op/cron transitions, seeded once on upgrade): `markets_in_dispute_window`, `disputes_awaiting_response`, `disputes_awaiting_decision` — the live counts an Oracle Console badges; drill into the lists with `list_markets_in_dispute_window` / `list_oracle_disputes`. +- **Computed-on-read** (time-dependent, so not stored): `markets_awaiting_resolution` + `oldest_unresolved_age` (this oracle's `status 1` markets past betting close, and the age of the oldest — one `by_oracle_status` walk), and `resolution_time_p50` / `resolution_time_p95` (percentiles read off the oracle's 8-bucket latency histogram `resolution_time_hist`, upper bucket edge where the cumulative count first crosses the percentile). + +**`pm_market_weight_sums`** — per side/outcome `bets_sum` and `weight_sum` (weight sums are computed by scanning bets, since they are not stored): +``` +{ market_type: uint8, bets_sum: share_type, + outcomes: [ { outcome_index, label, bets_sum, weight_sum } ] } +``` + +**`pm_dispute_votes`** — the committee tally **plus a stake-weighted projection of the finalize cron**, +so a caller can show the live quorum status and the verdict that would be applied under the current votes: +``` +{ votes: pm_dispute_vote_object[], + // legacy rough tally (weight = |vote_percent|, NOT stake) — kept for compatibility + uphold_weight, challenge_weight, total_weight, + challenger_leads: bool, // ≥ pm_dispute_approve_min_percent (rough) + proposed_outcome: int16, + // ── accurate stake-weighted projection (mirrors pm_dispute_finalize) ── + // every *_shares value is vesting-shares: effective_vesting_shares + lazy-pool stake → shares + participation_shares, // Σ weight of accounts that voted (= max_rshares) + electorate_shares, // total_vesting_shares + pool_NAV→shares (quorum base) + quorum_required_shares, // electorate × pm_dispute_approve_min_percent + quorum_percent_bp: int32, // participation / electorate (bp, 10000 = 100.00%) + quorum_reached: bool, // participation_shares ≥ quorum_required_shares + oracle_defense_shares, change_shares, // rshares defending the oracle vs. backing a change + outcome_change_shares: int64[], // per-outcome backing rshares (size = outcome_count) + expected_uphold: bool, // true ⇒ oracle resolution stands if finalized now + expected_outcome: int16, // outcome that would be set at finalize now + expected_consensus_strength_bp: int32 } // winning / participation (bp); 0 when uphold +``` +> The projection uses the **same** stake weighting and lazy-pool→shares bridge as the on-chain +> `pm_dispute_finalize`, so `expected_outcome` / `quorum_reached` match what the cron will apply at +> `voting_end_time` *given the votes cast so far* (votes are revisable until then — see +> [dispute operations](../protocol/operations/prediction-markets.md)). + +**`pm_kline`** — one charting point (per-outcome weight snapshot at a moment in time): +``` +{ seq: uint32, // 0-based, contiguous, monotonic per market (the change index) + timestamp: uint32, // unix seconds — x coordinate + reason: uint8, // 0 bet, 1 cancel, 2 liquidation, 3 batch settle, 4 leverage open, 5 leverage resolve + bets_sum: share_type, // total staked across all outcomes at this point + weights: share_type[] }// per-outcome staked weight (y values), index = outcome_index +``` + +**`pm_market_full`** — one-call enriched market view (`oracle`/`meta` are `null` when absent; the `my_*` +arrays are empty unless an `account` argument was supplied): +``` +{ market: pm_market_object, + outcomes: pm_outcome_object[], // empty for binary markets + weight_sums: pm_market_weight_sums, + oracle: pm_oracle | null, + meta: pm_market_meta_object | null, + my_positions: pm_position[], // account's bets on THIS market + my_leverage_positions: pm_leverage_position_object[], + my_liquidity: pm_liquidity_object[] } +``` + +**`pm_leverage_quote`** — leverage-open preview (from `pm::leverage::*`, the same math the evaluator runs): +``` +{ available: bool, outcome_index, collateral, + max_loan, max_leverage_x100, // 100 = 1.00× + pool_free_amount, fund_available, per_position_cap, market_position_cap, + pool_profit_percent, safety_margin_percent, max_slippage_percent, m_factor_percent, + expiration_buffer_sec, auto_close_time, // betting_expiration − buffer + stops: [ { leverage_x100, loan, total_bet, expected_tokens, pool_profit, + liquidation_threshold, current_cancel_value, worst_case_cancel_value } ], + failed_constraints: [ { constraint, reason } ] } // populated when !available +``` +**`pm_leverage_close_preview`** — `{ position_id, outcome_index, cancel_value, pool_obligation, bettor_receives, collateral, loan, pool_profit_charge, closeable: bool, loss_vs_collateral, loss_percent_bp }`. +**`pm_leverage_convert_preview`** — `{ position_id, outcome_index, cancel_value, pool_obligation, current_profit, conversion_profit_cost_percent, conversion_fee, total_user_payment, convertible: bool }`. + +**`pm_market_categories`** — browse taxonomy with live counts: +``` +{ categories: [ { category, count, subcategories: [ { subcategory, count } ] } ], // sorted by count desc + hot_tags: [ { tag, count } ] } // top 20 (jurisdiction-* excluded) +``` + +--- + +## Example + +Fetch a bettor's positions: +```bash +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_account_positions",["alice",0,100]]}' \ + http://127.0.0.1:8090 +``` + +Fetch the latest 1000 chart points for market `42`, then the previous 1000: +```bash +# newest page +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_market_kline",[42,0,1000]]}' http://127.0.0.1:8090 +# one page older +curl -s --data '{"jsonrpc":"2.0","id":1,"method":"call", + "params":["prediction_market_api","get_market_kline",[42,1000,1000]]}' http://127.0.0.1:8090 +``` + +Thin-client charting (lazy-load older history on scroll-back), turning each point into +per-outcome series of `{ x: unixtime, y: weight }`: +```js +async function call(method, params) { + const r = await fetch('http://127.0.0.1:8090', { method: 'POST', + body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'call', + params: ['prediction_market_api', method, params] }) }); + return (await r.json()).result; +} + +// Pull pages of 1000 from newest backwards until we have `want` points (or run out). +async function loadKline(marketId, want = 3000) { + const points = []; + for (let from = 0; points.length < want; from += 1000) { + const page = await call('get_market_kline', [marketId, from, 1000]); + if (!page.length) break; // reached the start of history + points.unshift(...page); // pages are ascending; prepend older pages + if (page.length < 1000) break; + } + return points; +} + +// One {x,y} series per outcome — feed straight into any charting lib. +function toSeries(points, outcomeCount) { + const series = Array.from({ length: outcomeCount }, () => []); + for (const p of points) + for (let i = 0; i < outcomeCount; i++) + series[i].push({ x: p.timestamp, y: Number(p.weights[i]) }); + return series; +} +``` + +See [Prediction Market Operations](../protocol/operations/prediction-markets.md) for the on-chain objects these methods expose, and [Chain Properties](../governance/chain-properties.md) for the v5 governance parameters. diff --git a/docs/plugins/validator.md b/docs/plugins/validator.md index c331de515a..28e9b54d7d 100644 --- a/docs/plugins/validator.md +++ b/docs/plugins/validator.md @@ -24,6 +24,7 @@ chain::plugin, p2p::p2p_plugin, snapshot::snapshot_plugin | `private-key` | — | WIF private key(s) for signing; may be repeated | | `emergency-private-key` | — | WIF key for emergency consensus; auto-adds `CHAIN_EMERGENCY_VALIDATOR_ACCOUNT` to the validator set | | `enable-stale-production` | `false` | Bypass participation and sync checks (testnet / network recovery only) | +| `disable-minority-fork-detection` | `false` | Skip minority-fork detection entirely (single-operator testnet/fork only). Never auto-cleared by healthy participation — see [Minority Fork Detection](#minority-fork-detection) | | `required-participation` | `3300` | Minimum validator participation in **basis points** (3300 = 33%) | | `fork-collision-timeout-blocks` | `21` | Consecutive fork-collision deferrals before forcing production (one full validator round) | @@ -116,7 +117,8 @@ When a competing block exists at `head_block_num + 1`: Before each production attempt (after HF12 safety checks), the plugin walks the last 21 blocks in `fork_db`. If all 21 were produced by the node's own configured validators, the node is isolated on a minority fork. - **Default action:** Call `p2p().resync_from_lib()` — pop blocks to LIB, reset fork DB, re-initiate P2P sync, reconnect seed nodes. Returns `minority_fork`. -- **With `enable-stale-production=true`:** Log a warning, continue producing. +- **With `enable-stale-production=true`:** Log a warning, continue producing. **Note:** at ≥33% participation this override is auto-cleared every block, so on a single-operator fork it does *not* stop the detector — use `disable-minority-fork-detection` instead. +- **With `disable-minority-fork-detection=true`:** Both the standard and DLT detection paths are skipped entirely, and the flag is never auto-cleared. For single-operator testnet/forks where "21 blocks all ours" is the healthy steady state. **Never enable on a real public network** — it removes the isolation guard. - **Skipped when:** Emergency consensus is active (committee blocks would always match our configured set). A DLT-specific slave isolation check replaces it in emergency mode. --- @@ -217,7 +219,7 @@ Included in P2P FORWARD stagnation logs when the node is stuck with no peer ahea | `no_private_key` | Config missing `private-key` for the signing key that's registered on-chain | | `low_participation` | Network participation < 33%; check peer connectivity or set `enable-stale-production=true` | | `fork_collision` | Competing block at next height; wait for vote-weight resolution or 21-deferral timeout | -| `minority_fork` | Isolated; plugin auto-resyncs to LIB | +| `minority_fork` | Isolated; plugin auto-resyncs to LIB. On a single-operator fork this loops — set `disable-minority-fork-detection=true` | | Watchdog fires repeatedly | Sync or catchup flag stuck; watchdog auto-clears if head is advancing | | `SLOT-HIJACK` logs | Emergency master blanked our key; restore via `validator_update_operation` | diff --git a/docs/prediction-markets/concepts-analysis.md b/docs/prediction-markets/concepts-analysis.md new file mode 100644 index 0000000000..361daa7d33 --- /dev/null +++ b/docs/prediction-markets/concepts-analysis.md @@ -0,0 +1,244 @@ +--- +title: Concept analysis — Onix vs the 90 prediction-market concepts +description: How the live VIZ on-chain implementation (Onix protocol, Forecaster client) maps onto the 90 prediction-market theory concepts — what is solved, inherent, not needed, or roadmap. +--- + +# Concept analysis — Onix vs the 90 theory concepts + +> **Forecaster** is the thin client to the VIZ on-chain prediction market — the access layer that lets +> people anywhere participate by signing `pm_*` operations (see the [section overview](./)). +> **Onix** is the protocol it talks to. This page maps each of the **90 PM-Atlas prediction-market +> concepts** onto **how the live VIZ on-chain implementation addresses it**, and **whether it is needed** +> for this architecture. +> +> Grounding docs: [whitepaper](./whitepaper), [specification](./specification), +> [workflows & disputes](./workflows). + +## Legend + +| Mark | Meaning | +|------|---------| +| ✅ **Resolved** | Onix design directly solves or handles it | +| ⚪ **Inherent** | A property Onix exhibits/inherits by construction (no extra work) | +| ➖ **Not needed** | Architecturally unnecessary under Onix | +| 🟡 **Partial / Roadmap** | Partly addressed today; rest is on the VIZ roadmap | +| 🏛 **Client-layer** | Handled by the jurisdictional client, not the protocol | +| 🔴 **Open / Risk** | Still a live concern; not fully solved | + +The single biggest structural difference from every other platform: **LP principal is structurally guaranteed (winners paid only from losers' forfeited stakes), pricing is CPMM for binary and LMSR-softmax + parimutuel settlement for multi, and there is no order book.** Most "liquidity & trading" concepts that exist to manage market-maker inventory risk simply **do not apply** because Onix has no inventory-bearing maker. + +> **On-chain actualization (HF14 / live).** This mapping was first written against the whitepaper/spec. Several items then marked *roadmap* are now **implemented as consensus operations** and verified in `consensus_sim`: +> - **Batch auctions + commit-reveal betting** — `pm_commit_bet` / `pm_reveal_bet` / `pm_batch_settle`, per-market `allow_batch` / `allow_instant_bet`, median kill-switch `pm_commit_reveal_enabled`. **Binary only** (multi forces `allow_instant_bet` — no LMSR batch yet). +> - **Opt-in leverage subsystem** — `pm_leverage_open/close/convert`, lazy-pool-funded, kill-switch `pm_leverage_enabled` (default off). Directly answers **Position Collateralization (#30)**. +> - **The Lazy Pool itself** — singleton, auto-allocation, MasterChef accounting, leverage loans, graduated recall. +> - **`endogeneity_tier`** market field; on-chain **creator bans** (`pm_creator_ban_object`); richer settlement virtual ops (`pm_payout` per bettor, `pm_leverage_resolve`, `pm_market_accepted`, `pm_auto_payout`) + matching plugin API. +> - **New since the spec:** **lazy-pool stake counts as governance weight** in PM disputes *and* DAO committee-request voting (converted to vesting-shares, HF14-gated). +> +> **Deliberately NOT built:** commit-reveal *dispute* voting — committee disputes are **open public hearings by design** (votes stay public via `pm_dispute_vote`, and ballots are revisable until close). **Still roadmap:** automated exogenous data oracles. Rows and the mitigations table below are updated to this live state. + +--- + +## 1. Information Theory + +| # | Concept | Verdict | How Forecaster-on-VIZ handles it / is it needed | +|---|---------|---------|--------------------------------------------------| +| 1 | **Brier Score** | ⚪ Inherent | Not a protocol mechanism but the *metric* by which Onix markets are judged. On VIZ, every bet/resolution is a consensus-validated event, so per-market price histories and outcomes are fully on-chain → Brier scoring of the platform (and of oracles) is computable by anyone. Needed only as an analytics/reputation input, not core logic. | +| 2 | **Calibration** | ⚪ Inherent | Onix prices are genuine probabilities (CPMM `P(A)=reserve_b/(reserve_a+reserve_b)`, LMSR softmax sums to 1). Calibration is an emergent property to *measure*, improved indirectly by the time-penalty (discourages no-info last-second bets) and deep LP liquidity. Not something the protocol enforces. | +| 3 | **Credibility Markets** | 🟡 Partial | The bonded-oracle + 14-metric reputation + composite trust score is effectively a credibility market for *resolvers*. Staking credibility against an outcome is native. A general "stake reputation on claims" product is a possible client-layer build, not core. | +| 4 | **Distribution Markets** | 🟡 Roadmap | Onix Multi (3–10 discrete outcomes) approximates a distribution via bucketed outcomes. True continuous distribution markets (CDF/scalar) are **not** in scope today; would need a scalar-outcome operation. Listed-adjacent to the roadmap's "category-level AMMs." | +| 5 | **Endogeneity** | 🟡 Partial (mitigated) | The risk that the market changes the thing it predicts. Category-specific (econ-data clean, political/social risky). Mitigated by the **live `endogeneity_tier` market field** (oracle-tagged 1/2/3) and **opt-in commit-reveal/batch betting (now on-chain)** that stops the public price from "leaking" mid-window (the thermostat channel); exogenous resolution via automated data oracles is **still roadmap**. See [Mitigations §](#mitigations-for-the-reflexivity-family). | +| 6 | **Forecasting Accuracy** | ⚪ Inherent | The whole value proposition. Onix improves it indirectly: zero-risk LP → deeper books → less slippage → more informed participation → better prices. Accuracy is the output to measure, not a feature to build. | +| 7 | **Info Finance** | ⚪ Inherent | Onix *is* an info-finance instrument: consensus-level operations turn information into priced, settleable positions. VIZ migration makes the information layer censorship-resistant and composable. | +| 8 | **Information Aggregation** | ✅ Resolved | Core function. CPMM/LMSR pricing aggregates dispersed bets into a single probability; deep risk-free LP liquidity is precisely the lever Onix pulls to make aggregation work (the flywheel in §7.3 of the whitepaper). | +| 9 | **Information Asymmetry** | 🟡 Partial | Onix's parimutuel/CPMM design means informed traders extract from *other losing bettors*, not from the LP — so asymmetry doesn't bankrupt liquidity (unlike CLOB/LMSR makers). **Opt-in commit-reveal + batch betting is now live (binary):** committed bets settle at a uniform batch price, removing the mempool-direction leak; per-market `allow_batch` + median kill-switch keep it optional. | +| 10 | **Legibility** | ✅ Resolved | Every financial action is a consensus-validated VIZ operation with a `market_log` audit trail (before/after reserves). Fully legible/auditable by any node — strictly more legible than a centralized backend or opaque CLOB. New settlement virtual ops (**`pm_payout`** per bettor, **`pm_leverage_resolve`**, **`pm_market_accepted`**) + dedicated plugin API methods make per-bettor outcomes and leverage resolutions directly queryable. | +| 11 | **Longshot Bias** | 🟡 Partial | CPMM/LMSR pricing can still exhibit favorite-longshot bias from bettor behavior; Onix doesn't correct it directly. The time penalty and deep liquidity dampen distortion, but bias is a behavioral output, not eliminated. | +| 12 | **Noise Decomposition** | ⚪ Inherent | Analytical lens, not a protocol feature. On-chain price/volume series on VIZ make signal-vs-noise decomposition feasible for analysts. Not needed in core. | +| 13 | **Nowcasting** | ⚪ Inherent | Onix prices update continuously per bet (~3s VIZ blocks), giving real-time nowcast estimates. Inherent to any live AMM market; no extra mechanism. | +| 14 | **Price Discovery** | ✅ Resolved | CPMM and LMSR-softmax are continuous price-discovery engines; price coherence (`Σ price = 1`) holds *by construction* with no arbitrage/split-merge layer needed. | +| 15 | **Probability Infrastructure** | ✅ Resolved | This is essentially Onix's thesis on VIZ: prediction markets as **first-class consensus operations** (`pm_*`), not smart contracts — a base-layer probability primitive. Directly the migration goal. | +| 16 | **Superforecasting** | ⚪ Inherent | Individual-skill concept; Onix rewards accurate bettors via the losers→winners payout. Position transfers + reputation could support superforecaster identity, but it's a participant trait, not protocol logic. | +| 17 | **Wisdom of Crowds** | ✅ Resolved | The mechanism Onix monetizes. Risk-free LP lowers the barrier so more of the crowd participates, sharpening the aggregate. Core to the design rationale. | +| 18 | **Yes Bias** | 🟡 Partial | Behavioral tilt toward "Yes." Onix's symmetric CPMM and profit-only time penalty don't structurally favor Yes, but they don't correct human bias either. Mitigated by liquidity depth; a measurement concern. | + +--- + +## 2. Mechanism Design + +| # | Concept | Verdict | How Forecaster-on-VIZ handles it / is it needed | +|---|---------|---------|--------------------------------------------------| +| 19 | **Binary Contracts** | ✅ Resolved | Onix Binary = CPMM (`x·y=k`) on two outcomes. AM-GM proof guarantees `reserve_a+reserve_b ≥ L`, so LP principal is covered. This is the primary market type. | +| 20 | **Combinatorial Prediction Markets** | ➖ Not needed (today) | LMSR decomposes naturally over combinatorial spaces, but Onix Multi caps at 3–10 *independent* outcomes and deliberately omits CTF split/merge. Combinatorial/conditional bundles are explicitly out of scope; not required for the LP-guarantee model. | +| 21 | **Incentive Compatibility** | ✅ Resolved | LMSR inherits IC from the log scoring rule (truth-telling dominant). Onix adds incentive alignment via bonded oracles (insurance > manipulation profit), losers-fund-winners settlement, and time-weighted LP rewards. | +| 22 | **Keynesian Beauty Contest** | ✅ Resolved (market) / ⚪ (dispute layer — accepted by design) | KBC is a pathology of *relative/peer scoring*. The Onix **market** layer pays bettors against external ground truth (parimutuel), so it is structurally anti-KBC — you profit by *deviating* from the crowd price when it's wrong. The only KBC exposure is the **stake-weighted committee dispute vote** (a peer mechanism). **Decision: commit-reveal dispute voting will NOT be implemented** — a committee dispute is an **open public hearing**, and the DAO's credibility depends on resolving disputes as transparently as possible; hiding ballots would erode that trust. The residual KBC risk is accepted and structurally small: voters are **not paid** for matching the majority (no bandwagon bounty) and **ballots are revisable** until close (so honest updates on new evidence are expected, not suppressed). Pooled DAO members are also **enfranchised** (lazy-pool stake → vesting-shares, HF14). See [Mitigations §](#mitigations-for-the-reflexivity-family). | +| 23 | **LMSR** | ✅ Resolved (the key innovation) | The concept file notes LMSR *failed for binaries* (permanent loss on the 0/1 boundary). Onix's answer: **use CPMM for binary, and use LMSR only for multi where the maker is NOT the counterparty** — parimutuel settlement pays winners from losers, so the LMSR subsidy is never at risk (`LP max loss = 0` vs `b·ln(N)`). This is the central design move. | +| 24 | **LOX (Log-Odds Excess Lateness)** | ➖ Not needed | A specialized scoring/lateness metric. Onix instead uses a **quadratic time penalty on profit** to handle late-bet incentives — a simpler, settlement-time mechanism. LOX scoring is not part of the model. | +| 25 | **Market Manipulation** | 🟡 Partial | Bonded oracle (bond must exceed manipulation profit), DPoS-validated operations, and committee dispute arbitration raise manipulation cost. Price manipulation via large bets is bounded by depth and — on **batch/commit-reveal markets (now live)** — by uniform-price settlement that neutralises speed-based sniping (the "sniper's tax"); active surveillance is still roadmap. | +| 26 | **Market Scoring Rules** | ✅ Resolved | Onix Multi is a market scoring rule (LMSR) implementation, repurposed with parimutuel settlement. Directly used. | +| 27 | **Multi-Outcome Markets** | ✅ Resolved | Onix Multi handles N=3–10 via LMSR softmax pricing + parimutuel payout, with `b = S/ln(N)`. First-class market type. | +| 28 | **Parimutuel Markets** | ✅ Resolved (foundational) | Settlement in *both* market types is parimutuel: losers' forfeited stakes form `winners_pool`, distributed by token share. This is what makes the LP guarantee structural rather than insured. | +| 29 | **Peer Prediction** | ➖ Not needed | Truth-telling-without-ground-truth schemes. Onix relies on bonded oracles + committee dispute, not peer-prediction scoring. Could inform subjective-market resolution but not used. | +| 30 | **Position Collateralization** | ✅ Resolved (+ opt-in leverage, live) | By default every bet is fully prepaid (full amount enters reserves; no fees at bet time) — total collateralization by construction. Onix now **also** ships the concept's "next level": an **opt-in leverage subsystem** (`pm_leverage_open/close/convert`, kill-switch `pm_leverage_enabled`, default off). Margin is a **loan from the Lazy Pool** (no token emission — zero-sum preserved), so the position stays fully collateralized *from the system's view*. The binary "jump risk" that breaks CLOB liquidation engines (per this concept) is handled by **liquidating against pre-bet reserves**: opposing-bet / settlement force-close recovers `min(cancel_value, obligation) ≥ loan`, so the pool gets loan + interest back; the **only** bounded bad-debt path is a same-side `pm_cancel_bet` (Case B). The liquidation cascade is deliberately **not** gated by the kill-switch, so toggling leverage off never strips protection from open positions. | +| 31 | **Proper Scoring Rules** | ✅ Resolved | LMSR is the cost-function dual of the log proper scoring rule; Onix Multi inherits its truthful-elicitation property. | +| 32 | **Reflexivity** | 🟡 Partial (mitigated) | Parent of endogeneity. Mitigated by the same toolkit — **commit-reveal/batch betting + `endogeneity_tier` are now live**, exogenous resolution still roadmap — **plus on-chain creator bans** (`pm_creator_ban_object`) for harmful reflexivity (assassination/"hit" markets, propaganda markets that create a "constituency for the outcome"); the prohibited-category *list* itself stays client-layer. Deep risk-free LP depth also raises the cost of newsworthy price manipulation. See [Mitigations §](#mitigations-for-the-reflexivity-family). | + +--- + +## 3. Liquidity & Trading + +> **Headline:** Onix has **no order book and no inventory-bearing market maker**. A large class of these concepts exists specifically to manage CLOB/maker inventory risk and therefore **do not apply** to Onix. + +| # | Concept | Verdict | How Forecaster-on-VIZ handles it / is it needed | +|---|---------|---------|--------------------------------------------------| +| 33 | **Adverse Selection** | ✅ Resolved (reframed) | The classic problem (informed flow bankrupts the maker) **cannot bankrupt the Onix LP**: winners are paid from losers, never from LP principal (AM-GM / parimutuel guarantee). Informed traders extract from other *bettors*, not the LP. Eliminates the core LP failure mode. | +| 34 | **Arbitrage** | ⚪ Inherent | Intra-market arbitrage is unnecessary: price coherence (`Σ price = 1`) holds by construction in both CPMM and LMSR-softmax. No split/merge arbitrage layer needed. | +| 35 | **Batched Auctions** | ✅ Resolved (opt-in, live) | Implemented as a **per-market uniform-price batch** (`mode=1` bets + commit-reveal → `pm_batch_settle` at each `pm_batch_epoch_blocks` epoch); only the **net residual** moves the AMM, so all same-side fills clear at one price and speed-based sniping (the "sniper's tax") is neutralised. Per-market `allow_batch` + median kill-switch `pm_commit_reveal_enabled`; **binary only** today (multi forces `allow_instant_bet`). The LP `Σreserve ≥ L` invariant is untouched — each batch is one valid CPMM transition. | +| 36 | **Bid-Ask Spread** | ➖ Not needed | No order book → no quoted spread. "Cost of trading" appears as CPMM/LMSR slippage, governed by liquidity depth, not maker spreads. Concept doesn't map. | +| 37 | **Bonding Trades** | ⚪ Inherent | Bets *are* bonded trades: capital is committed into reserves and only released at resolution (or via cancellation/transfer). Native behavior. | +| 38 | **Continuous Double Auction** | ➖ Not needed | CDA is the CLOB model Onix explicitly rejects in favor of AMM pricing. Not used. | +| 39 | **Covariance Markets** | ➖ Not needed | Trading correlation between events requires combinatorial/conditional structure Onix deliberately omits. Out of scope. | +| 40 | **Cross-Platform Arbitrage** | 🟡 Partial | Onix prices can diverge from Polymarket/Kalshi; arbitrage across platforms is possible but external to the protocol. VIZ's open API + headless client make price data accessible; no native cross-platform bridge. | +| 41 | **Execution Quality** | ✅ Resolved (reframed) | No partial fills/queue position. Execution quality = deterministic slippage + optional `min_tokens`/`min_return` slippage guards, validated at consensus. Predictable by construction. | +| 42 | **Gap Risk** | ✅ Resolved (for LP) | Gap risk (sudden jump to 0/1 wiping the maker) is the failure mode Onix's structural LP guarantee eliminates — the LP never holds the losing side's terminal risk. Bettors still bear their own outcome risk (as intended). | +| 43 | **Hedging** | 🟡 Partial | Bettors can hedge by taking offsetting positions, transferring positions (`pm_transfer_position`), bet cancellation (if allowed) via reverse CPMM, and now **opt-in leverage** (`pm_leverage_open/convert`) for capital-efficient offsetting. No native multi-leg derivatives; basic + leveraged hedging is possible. | +| 44 | **Implied Correlation** | ➖ Not needed | Requires multi-event/combinatorial markets Onix omits. Out of scope. | +| 45 | **Insider Trading** | 🟡 Partial / 🏛 Client | Protocol can't detect insider info; mitigated by time penalty (late-info bets earn less profit) and bonded-oracle resolution. KYC/surveillance to police insiders is a **client-layer** responsibility (regulated clients). | +| 46 | **Kelly Criterion** | ⚪ Inherent | A bettor staking strategy, not a protocol feature. Onix exposes clean probabilities and full collateralization so Kelly sizing is computable by participants; the new **opt-in leverage** lets a bettor act on a fractional-Kelly edge with margin (pool-funded, liquidation-bounded). No core involvement beyond exposing the primitives. | +| 47 | **Liquidity Fragmentation** | 🟡 Roadmap | Per-market pools fragment liquidity today. The whitepaper's top-priority roadmap item — **shared/category-level AMM pools** — is the architectural fix. Lazy Pool already mutualizes *deposits* across markets. | +| 48 | **Liquidity Provision** | ✅ Resolved (core differentiator) | Risk-free LP is the headline: principal structurally guaranteed, time-weighted fee rewards, Lazy Pool auto-allocation + MasterChef accounting. Solves the "LPs lose money" problem that motivates the whole protocol. | +| 49 | **Market Making** | ✅ Resolved (reframed) | No active maker needed — the AMM + LP pool *is* the maker, and it bears no inventory risk. "Market making" collapses into passive, risk-free liquidity provision. | +| 50 | **Minimum Viable Liquidity** | ✅ Resolved | Enforced floor: min initial liquidity 100 VIZ; Lazy Pool auto-seeds every new market with `free_balance × allocation_%`. MVL is structurally bootstrapped rather than left to chance. | +| 51 | **Order Book** | ➖ Not needed | Onix is AMM-based; no order book by design. | +| 52 | **Orderflow Arbitrage** | ➖ Not needed | No order book / no PFOF-style flow routing → not applicable. | +| 53 | **Relative Value Trading** | ➖ Not needed | Cross-instrument RV requires correlated/combinatorial markets Onix omits. Out of scope. | +| 54 | **Retail Flow** | ✅ Resolved (reframed) | In CLOB models retail flow subsidizes maker losses to toxic flow. In Onix there is no maker to protect — retail and informed bettors all pay into the same parimutuel pool; LP is indifferent. The "retail-vs-toxic" tension dissolves at the LP layer. | +| 55 | **Semantic Tick Size** | ➖ Not needed | Tick granularity is a CLOB concept. Onix prices are continuous AMM functions; precision is the fixed mVIZ unit (1/1000). No tick design needed. | +| 56 | **Temporal Arbitrage** | 🟡 Partial | Betting earlier vs later carries different risk; Onix's **time penalty on profit** is precisely the mechanism that prices in lateness, dampening "wait-for-certainty" temporal arbitrage. Not eliminated, but explicitly disincentivized. | +| 57 | **Time Arbitrage** | 🟡 Partial | Same family as #56 — exploiting information timing. Quadratic time penalty + ~3s block cadence reduce, but don't remove, the edge. Addressed by design intent. | +| 58 | **Toxic Flow** | ✅ Resolved (for LP) | The defining CLOB/LMSR problem (sniper clears the book at 10¢ on a 99¢ outcome, maker eats 80¢) **does not hit the Onix LP** — payouts come from losers' stakes, and the LP subsidy is returned unconditionally. Toxic flow simply means informed bettors win the parimutuel pool, as intended. Major structural win. | +| 59 | **Wash Trading** | 🟡 Partial / 🏛 Client | No fees at bet time removes one wash incentive, but volume-faking is still possible; transfers are pure reassignment (no fee farming there). Detection/surveillance is a client + roadmap surveillance concern. | + +--- + +## 4. Oracle & Resolution + +| # | Concept | Verdict | How Forecaster-on-VIZ handles it / is it needed | +|---|---------|---------|--------------------------------------------------| +| 60 | **Corruption Value Multiple (CVM)** | ✅ Resolved (by design principle) | The protocol's explicit security invariant: oracle **insurance bond must exceed potential manipulation profit**. Risk factor (insurance/bets ratio) feeds the composite trust score. CVM is directly the bonding rationale. | +| 61 | **Dispute Resolution** | ✅ Resolved | Full system: 12h grace, `dispute_fee`, mandatory oracle response, per-market resolver (`dispute_mode==0` committee stake-weighted vote / `==1` named resolver), insurance slashing, 3-outcome no-contest disputes, 14-day auto-close anti-freeze, on-chain creator/oracle bans. Among the most fully specified parts. **HF14 addition:** lazy-pool depositors keep their dispute vote weight (pool NAV → vesting-shares, added to `effective_vesting_shares`). | +| 62 | **Oracle Design** | ✅ Resolved | Bonded oracle model: registration fee, ≥5000 VIZ insurance, explicit acceptance, evidence-backed resolution, 14-metric reputation, freshness decay, ban mechanics. Core subsystem. | +| 63 | **Resolution Criteria** | 🟡 Partial / 🏛 Client | Market question/criteria live in `url`/description (custom_json, display-only). Protocol enforces *process* (who resolves, disputes) but not criterion *quality* — ambiguous criteria are a creator/client responsibility, policed retroactively via disputes + **on-chain creator bans** (`pm_creator_ban_object`, now live). | +| 64 | **Self-Resolving Markets** | ➖ Not needed (today) | Onix resolution is oracle-driven, not algorithmic self-resolution. Automated data oracles (Chainlink-style feeds) are a high-priority roadmap item for objective markets, which would approximate self-resolution. | +| 65 | **UMA Protocol** | ➖ Not needed (replaced) | UMA's optimistic oracle (used by Polymarket) is functionally replaced by Onix's bonded oracle + VIZ committee dispute model. Same problem, native VIZ solution — no external oracle dependency. | + +--- + +## 5. Governance & Decisions + +| # | Concept | Verdict | How Forecaster-on-VIZ handles it / is it needed | +|---|---------|---------|--------------------------------------------------| +| 66 | **Attention Markets** | ➖ Not needed | Trading attention/virality is a distinct product; Onix is event-resolution focused. Could be a client-layer market category, not core. | +| 67 | **Conditional Tokens** | ➖ Not needed (explicit) | Whitepaper §7.2 argues CTF split/merge is **architecturally unnecessary** — price coherence is mathematical, not enforced by tokens. The *one* useful CTF feature (transferable positions) is reimplemented natively as `pm_transfer_position` with encrypted memos. Deliberately omitted. | +| 68 | **Decision Markets** | 🟡 Possible | Onix Multi could express decision markets, but conditional "if-policy-then-metric" structure isn't native (no conditional tokens). Buildable at client layer; not a core primitive. | +| 69 | **Futarchy** | 🟡 Possible (client) | Concept file: futarchy = decision markets on conditional futures. Onix lacks native conditional markets, so full futarchy isn't supported in core. VIZ's stake-weighted committee already governs *parameters*; governance-by-market would be a client/roadmap construction. | +| 70 | **Hyperstition Markets** | ➖ Not needed (deliberate) | Reflexivity-as-a-feature (coordinate, don't forecast). This is a *design choice, not a bug to fix*: Onix's requirement that an outcome be **externally verifiable by a bonded oracle** structurally excludes hyperstition markets by default. Could exist as a separate client-layer "coordination market" product with milestone resolution, but is not a core target. See [Mitigations §](#mitigations-for-the-reflexivity-family). | +| 71 | **Impact Markets** | ➖ Not needed | Retrospective-funding/impact-certificate markets are a separate domain. Possible client-layer category; not core. | +| 72 | **No-Loss Prediction Markets** | 🟡 Adjacent | Onix isn't no-loss for *bettors* (losers forfeit stakes — that funds winners). But it **is** "no-loss" for *LPs* (principal guaranteed). The yield-funded no-loss variant (stake yield, principal returned) is a different model; LP-side no-loss is already delivered. | +| 73 | **Opportunity Markets** | ⚪ Inherent (adjacent) | The Lazy Pool's **opportunity-cost protection** (graduated recall, active-market penalty, fault stamps) addresses capital-opportunity-cost directly — though "opportunity markets" as a product category is out of scope. | + +--- + +## 6. Business & Platforms + +| # | Concept | Verdict | How Forecaster-on-VIZ handles it / is it needed | +|---|---------|---------|--------------------------------------------------| +| 74 | **AI agents** | 🟡 Roadmap | Headless client + open VIZ operations make programmatic agents (bettors, LPs, automated oracles) straightforward. AI-driven liquidity/resolution is a natural extension, not yet specified. | +| 75 | **Cross-subsidization** | ⚪ Inherent | The Lazy Pool cross-subsidizes liquidity across many markets from one deposit; MasterChef `reward_per_share` shares fee yield. Cross-subsidization is built into the pool economics. | +| 76 | **Demand markets** | ➖ Not needed | Markets that gauge/aggregate demand are a product category; not a core Onix primitive. Client-layer. | +| 77 | **Distribution moat** | 🟡 Strategy | Onix's moat is risk-free LP yield + VIZ-native infrastructure (the flywheel). Distribution (Telegram WebApp today → headless web client) is a go-to-market concern, partly addressed by platform-independence post-migration. | +| 78 | **Election markets** | 🏛 Client | Supported as ordinary binary/multi markets; their *legality* is a jurisdictional-client matter (whitelisted oracles, category filters). Protocol-neutral. | +| 79 | **Event contracts** | ⚪ Inherent | Every Onix market *is* an event contract. The regulatory classification of these contracts is a client/legal question, not protocol logic. | +| 80 | **Federal preemption** | 🏛 Client (N/A to protocol) | Concept file: turns on whether US event contracts are "swaps." VIZ DLT is **infrastructure, not an operator** (whitepaper §6.2) — like Bitcoin is a ledger. Legal obligations attach to clients, not consensus. Not a protocol concern. | +| 81 | **Long-tail markets** | ✅ Resolved | The exact niche LMSR's bounded-loss enables — and Onix makes it *risk-free* to seed via Lazy Pool auto-allocation + min-liquidity floor. Long-tail viability is a core selling point. | +| 82 | **Market structure** | ✅ Resolved (defined) | Onix defines a clear structure: AMM pricing, parimutuel settlement, bonded oracles, DPoS-governed parameters, consensus-level ops. A coherent, novel market structure vs CLOB platforms. | +| 83 | **Market surveillance** | 🟡 Roadmap / 🏛 Client | Full on-chain audit trail (`market_log`, every op consensus-validated) makes surveillance *possible* by anyone. Active surveillance/enforcement is a client + roadmap concern. | +| 84 | **Network effects** | 🟡 Strategy | The flywheel (risk-free LP → depth → bettors → fees → more LP) is the intended network effect. Shared liquidity pools (roadmap) strengthen it. Go-to-market, not protocol mechanics. | +| 85 | **Parlays** | ➖ Not needed | Multi-leg combined bets need conditional/combinatorial structure Onix omits. Out of scope (could be a client construction over independent markets). | +| 86 | **Platform competition** | 🟡 Strategy | Competes on the unique "passive yield without impermanent loss" angle vs Polymarket/Kalshi (whitepaper §7.1 comparison table). Strategic positioning, not protocol logic. | +| 87 | **Polymarket** | ⚪ Reference | The primary benchmark. Onix differs on every axis: CPMM/LMSR vs CLOB, zero LP risk vs inventory risk, native ops vs Polygon contracts, bonded oracle vs UMA, no CTF. Used as comparison, not adopted. | +| 88 | **Regulatory arbitrage** | 🏛 Client | Jurisdictional-client model means each region builds its compliant (or permissionless) client on neutral VIZ rails. Regulatory positioning lives entirely at the client layer. | +| 89 | **Regulatory classification** | 🏛 Client | Whether markets are swaps/gaming/securities is decided per-jurisdiction at the client layer; the protocol is classification-neutral (same `pm_*` ops for permissionless and regulated clients). Not a protocol concern. | + +--- + +## Summary — what the Onix-on-VIZ design actually changes + +**Solved structurally (the core wins):** +- LP-side **adverse selection, toxic flow, gap risk, impermanent loss, market-maker inventory risk** → all eliminated because winners are paid only from losers' forfeited stakes and LP principal is returned unconditionally (CPMM AM-GM proof; LMSR parimutuel settlement). +- **LMSR's binary-market failure** → sidestepped by using CPMM for binary and confining LMSR to multi-outcome markets where the maker is not the counterparty. +- **Liquidity provision, minimum viable liquidity, long-tail viability** → risk-free LP + Lazy Pool auto-allocation. +- **Oracle design, dispute resolution, CVM** → bonded oracle + 14-metric reputation + stake-weighted committee disputes. +- **Price discovery, arbitrage, conditional tokens** → price coherence is mathematical (`Σ price = 1`), so no order book, no split/merge, no internal arbitrage layer needed. + +**Not needed / deliberately omitted:** order book, CDA, bid-ask spread, semantic tick size, orderflow arbitrage, CTF split/merge, combinatorial/covariance/correlation/relative-value/parlay markets, UMA, peer prediction, LOX. + +**Pushed to the jurisdictional client layer:** federal preemption, regulatory classification/arbitrage, election-market legality, KYC/insider-trading enforcement, surveillance. + +**Newly implemented since the spec-era mapping (now live on-chain, HF14):** opt-in batch auctions + commit-reveal betting (binary), the opt-in leverage subsystem (position collateralization), the Lazy Pool, `endogeneity_tier`, on-chain creator bans, per-bettor/leverage settlement vops + plugin API, and lazy-pool governance weight in PM disputes + DAO committee-request voting. + +**On the VIZ roadmap (partial today):** shared/category liquidity pools (fixes fragmentation), automated data oracles (→ self-resolving objective markets), distribution markets, AI agents. *(Note: commit-reveal **dispute** voting is **not** on this list — it is deliberately rejected; dispute hearings stay public. Commit-reveal **betting** is already live.)* + +**Still open / behavioral (mitigated, not eliminated):** longshot/yes bias, market manipulation via depth, cross-platform arbitrage. The reflexivity family (endogeneity, reflexivity, KBC, hyperstition) has a concrete mitigation plan — see below. + +--- + +## Mitigations for the reflexivity family + +Endogeneity, reflexivity, the Keynesian beauty contest (KBC), and hyperstition are **one root problem at different layers**: the market/price influences the outcome it measures. One small set of primitives addresses all four. + +### Root-cause map + +| Layer | Concept | Channel | +|-------|---------|---------| +| Forecaster-level | **Keynesian Beauty Contest** | herding to visible consensus in *relative/peer scoring* | +| Market-level | **Endogeneity** | market *existence/visibility* changes behavior (category-specific) | +| Market-level | **Reflexivity** | general price↔reality feedback; manipulation-as-propaganda | +| By-design | **Hyperstition** | reflexivity used *intentionally* to coordinate an outcome | + +### Mitigation primitives + +| Primitive | Status | Fixes | Notes | +|-----------|--------|-------|-------| +| **Commit-reveal dispute voting** | **rejected (will NOT be built)** | KBC | A committee dispute is an **open public hearing**: `pm_dispute_vote` is a public ballot and **stays that way by design** — DAO credibility depends on transparent adjudication. Ballots are **revisable** until close (re-vote overwrites) so voters update honestly on new evidence; KBC residual is accepted (voters aren't paid for matching the majority). | +| **Commit-reveal betting (batched)** | **live (opt-in, binary)** | endogeneity, reflexivity, info-asymmetry | `pm_commit_bet`/`pm_reveal_bet`/`pm_batch_settle` hide in-flight order direction/size so the public price doesn't "leak" during the betting window (kills the thermostat channel). Settled as a uniform-price **batch** (see below). | +| **`endogeneity_tier` market field** | **live field** | endogeneity | Oracle tags tier 1 (econ data — clean), 2 (sports/scheduled), 3 (political/social — risky); UI surfaces the reflexive-risk level; clients can restrict tier-3. | +| **Exogenous resolution (automated data oracles)** | roadmap (high) | endogeneity, reflexivity | Resolution bound to an external feed (BLS/Fed/sports API) the market can't influence → clean thermometer. | +| **Prohibited-category list + creator ban** | **creator ban live on-chain**; list client-layer | harmful reflexivity, hyperstition | Block markets where YES creates a "constituency for the outcome" (assassination/"hit"/terror markets, propaganda markets). Enforced via on-chain creator ban (`pm_creator_ban_object`) + client category filter. | +| **Deep risk-free LP liquidity** | core today | manipulation-driven reflexivity | The flywheel makes the book deep, so moving price for a "newsworthy" manipulated headline is expensive. | + +### KBC: why the market layer is already safe + +KBC is a pathology of **relative scoring** (pay for closeness to peers → herd to peers). Onix's market layer pays bettors against **external ground truth via parimutuel settlement** — you are rewarded for *deviating* from a wrong crowd price, not for matching it. So the bettor layer is structurally anti-KBC. The only relative/peer mechanism in the protocol is the **stake-weighted committee dispute vote**, and the residual KBC risk there is **accepted by design** — the dispute is kept an open public hearing (no commit-reveal) because DAO credibility depends on transparent adjudication; what bounds the risk instead is that voters aren't paid for matching the majority and ballots stay revisable as evidence comes in. + +### Commit-reveal vs. the CPMM `a·b=k` invariant + +CPMM is **path-dependent** (tokens depend on reserves at execution time), so commit-reveal *cannot* be done bet-by-bet against the live curve — reveal ordering would re-introduce MEV and leak price. The fix (and why §8.3 pairs commit-reveal with the batch-auction model): **stop updating the curve per-bet; update it once per epoch via a uniform-price batch settlement.** + +1. **Commit:** submit `hash(side, amount, salt, min_tokens)` and escrow `amount`. +2. **Reveal:** reveal `(side, amount, salt)`; reveals are collected but **not applied** until epoch close (seeing others' reveals is useless — yours is already committed). +3. **Settle once:** opposing flow (`A_in` vs `B_in`) nets between bettors at a single clearing price `p*`; only the **net residual** moves the AMM, so `k` is recomputed **once**. All A-fills get `p*`, all B-fills get `1−p*` → no intra-batch ordering advantage. +4. **`min_tokens` floor:** since price is invisible at commit time, a per-bet token floor is mandatory; if `tokens < min_tokens` at settlement, the bet is rejected and refunded from escrow. +5. **Anti-griefing:** non-reveal forfeits a penalty from escrow to the LP-fee/DAO pool, killing the "commit optionality, reveal only winners" attack. + +**The LP guarantee is untouched:** each batch settlement is still a valid CPMM transition, so AM-GM `reserve_a + reserve_b ≥ L` still holds. Only the *granularity* of curve updates changes (per-bet → per-epoch). Onix Multi is analogous via the aggregated LMSR cost function. + +**Phasing:** (1) uniform-price batch auctions first — already kill ordering MEV/front-running cheaply; (2) commit-reveal hiding on top — adds in-flight confidentiality, enabled selectively for tier-3 (endogeneity-sensitive) markets. **Both (1) and (2) are now implemented on-chain for binary markets** (multi still forces instant betting — no LMSR batch yet). + +### Net verdict change + +| Concept | Before | After | +|---------|--------|-------| +| Keynesian Beauty Contest | 🔴 Open | ✅ market / ⚪ dispute — votes public **by design** (no commit-reveal; revisable ballots, unpaid voters, pooled voters enfranchised) | +| Endogeneity | 🔴 Open | 🟡 mitigated — `endogeneity_tier` + commit-reveal/batch **live**; exogenous oracles roadmap | +| Reflexivity | 🔴 Open | 🟡 mitigated — creator ban **live on-chain**; category list client-layer | +| Hyperstition | 🔴 Open | ➖ excluded by design (optional client product) | diff --git a/docs/prediction-markets/early-exit-deferred-claim.md b/docs/prediction-markets/early-exit-deferred-claim.md new file mode 100644 index 0000000000..860785806e --- /dev/null +++ b/docs/prediction-markets/early-exit-deferred-claim.md @@ -0,0 +1,132 @@ +# Early-exit deferred claim (F1 / #300) + +Status: design locked (owner 2026-08-08), implementation in progress on branch `pm`. + +## Problem + +The market is a hybrid: a CPMM (binary) / LMSR (multi) **curve** for entry and early exit, +and **pari-mutuel** settlement for positions held to resolution. Any round-trip through the +curve (buy then sell before settlement) realizes a trading P&L against the curve depth — the +LPs — exactly like Uniswap impermanent loss. But the design promises LPs **principal +protection** (fee-only, no IL). Those two are in tension. + +Two code paths exit against the curve on a binary market: + +- **Leverage** (`liquidate_position` / `pm_leverage_close`): always; force-closed at + settlement; amplified by the pool loan. +- **Regular bet cancel** (`cancel_bet`, F2 curve-priced refund): during the betting window. + +Both route `residual = stake − curve_refund` to `forfeit_pool` (signed). When an early exit is +*profitable* (`curve_refund > stake`), `forfeit_pool` goes **negative**. At settlement +`winners_pool = losers_sum − fees + forfeit_pool`; if leverage/early-exit profit outran the +losing stakes, `winners_pool < 0`, is floored to 0, and the shortfall (`uncovered`, F1) is +charged to LP principal — or minted when LP principal is exhausted. Reachable: proven with a +gate-respecting CPMM simulation (one-sided pump, `uncovered = 7316`); Babin's replay corpus hit +it in 1163/1988 pairs. + +Root cause: **curve-priced exit pays a bonding-curve value that is not bounded by the losing +pool**, while settlement pays pari-mutuel. The gap lands on the LP. + +## Model (locked) + +Early exits no longer extract curve value from LPs. Instead an exit records an +**outcome-contingent deferred claim**, funded at settlement from a **bounded slice of the +losing pool**. + +### Recorded on exit +`{ position_id, kind (bet|leverage), chosen_outcome, claim_amount, exit_time }`. + +### Regular bet cancel +- **Principal returned immediately, unconditionally**: `refund = min(curve_refund, stake)` + (own money, not borrowed). A cancel can cut losses or break even but never realizes + curve-profit at cancel time. +- **Profit tail** `max(curve_refund − stake, 0)` → deferred claim on the chosen outcome. +- **Depth-normalized pricing (audit #1-C):** the capped/tail split is re-priced at the bet's + *entry* curve depth, not the current one. `pm_bet_object.entry_liquidity` records + `liquidity_sum` at the moment the CPMM bet hit the curve (instant `place_bet` and batch fill). + On cancel both reserves are scaled by `entry_liquidity / liquidity_sum` (mirror-of-buy on the + scaled reserves). Because bets keep `k` invariant while liquidity ops scale `k` by `f²` and + `liquidity_sum` by `f`, `sqrt(k_entry / k_now) == L_entry / L_now` **exactly** → deterministic, + sqrt-free (no integer sqrt). The result is **clamped to the real `curve_refund`** so + normalization may only *reduce* the payout, never raise it. This kills the self-liquidity + inflation vector (bet → own `add_liquidity` inflates depth → larger `curve_refund` → cancel + mints a bigger tail → withdraw returns the liquidity whole = cash-neutral guaranteed profit) + without opening a shrink-side one. Falls back to legacy pricing when `entry_liquidity` is + absent (bets predating the field). + +### Leverage close / liquidate +- **Collateral is NOT separately returned** — it is first-loss margin for the pool. The pool + recovers its obligation (`loan·(1+R) + funding`) from `cv` first; if `cv < obligation`, the + collateral covers the gap. +- **Residual** `max(cv − obligation, 0)` → deferred claim on the chosen outcome. +- Leverage is therefore a **leveraged directional bet**, not a volatility harvest: you profit + only if your outcome wins and the bucket has room; a wrong outcome loses the collateral. +- NB two distinct predicates now: **solvency** (`cv ≥ obligation`, governs loan recovery) vs + **outcome-win** (governs the right to a claim). A position can be solvent yet on the losing + outcome → pool made whole, claim = 0. + +### Settlement +1. `bucket = pm_early_exit_reward_cap_percent × losers_sum / 10000` (default 33%). +2. Collect deferred claims on the **winning outcome only** (losing-outcome claims → 0). +3. Pay them **FIFO by `exit_time`** (first out, first paid) until the bucket is drained; no + per-position cap (owner 2026-08-08: FIFO ordering + total bucket is the bound). A claim that + the remaining bucket cannot fully fund is paid partially; the rest is unpaid (haircut). +4. **Any unused bucket returns to the winners' pool** — held winning bets share it pari-mutuel. + +### Guarantees +``` +paid_claims ≤ bucket = cap · losers_sum +winners_pool = losers_sum − fees − paid_claims + honest_forfeits + ≥ (1 − cap) · losers_sum − fees ≥ 0 (cap < 100%) +``` +- `uncovered` is **impossible by construction**; no mint; **LP principal never touched**; the + lazy pool bears no leverage IL. +- A **losing outcome never profits** (owner requirement). +- Held winners receive `≥ (1 − cap)` of the losing pool plus any unused bucket. +- Early exit is a **bounded, contingent discount** (≤ cap, FIFO) vs holding to resolution + (full pari-mutuel share) → no arbitrage against holding; a deliberate liquidity discount. + +## Chain parameter + +`pm_early_exit_reward_cap_percent` (uint16, bp, default **3300** = 33% of `losers_sum`). +Median-voted validator param; `validate()` bounds `≤ 10000`. Added to +`chain_properties_pm` + FC_REFLECT + `calc_median` (DONE, single-TU verified). + +## Implementation touchpoints (node) + +- [x] chain param `pm_early_exit_reward_cap_percent` (struct/validate/reflect/median). +- [ ] object `pm_deferred_claim_object` (+ index by market, by exit_time) — space 30, append at + end of `object_type` enum (snapshot-safe, like `pm_lazy_withdraw_request`). +- [ ] `cancel_bet`: return `min(curve_refund, stake)`, record profit-tail claim; stop routing a + negative residual to `forfeit_pool`. +- [ ] `liquidate_position` / `pm_leverage_close`: pool takes obligation, record `cv − obligation` + claim, tag chosen outcome; drop immediate `bettor_received`; stop negative `forfeit_pool`. +- [ ] settlement (`settle_market`): after force-close, compute `bucket`, pay winning-outcome + claims FIFO by exit_time, remainder → winners' pool; remove the `uncovered`/F1 + `settle_liquidity` charge path (LP no longer absorbs it). +- [ ] snapshot: include `pm_deferred_claim_object` in allowlist (+ import handler). +- [x] virtual op `pm_early_exit_claim_paid` (account, market, kind, outcome, `claimed`, `paid`) — + appended at the end of the `operation` variant (op-ids stay stable), FC_REFLECT'd, emitted in + the settlement distribution loop next to the `adjust_balance`, and routed to the early-exiter's + account history (`account_history` impacted-accounts visitor). `claimed` vs `paid` exposes any + bucket-exhaustion haircut. `adjust_balance` alone leaves no history trace — this closes that gap. +- [x] read API: `get_deferred_claims(market, [from=0], [limit=100])` — FIFO exit order via + `by_claim_market`; empty on a settled market (claims consumed). Plugin-only (clients call via + rawApi/JSON-RPC), matching `get_lazy_withdraw_requests`; no wallet wiring. + +## Client / lib / docs follow-ups +- viz-js-lib / viz-php-lib / viz-python-lib: new chain param in v5 chain_properties_pm + (serialization lock-step, byte-verify), any new read method / vop. +- Forecaster: notices + operation descriptions (leverage = directional, early exit = bounded + discount), show pending deferred claim on positions. +- WebVIZWallet: same operation-description updates if surfaced. +- Scientific article: `early-exit choice` with the math (regular vs leverage-from-lazy-pool, + validator-set reward cap). + +## Rejected alternatives (why) +- Smear `uncovered` across all LP / mint (status quo) — breaks LP principal promise. +- Localize only to lazy pool — pool can be exhausted; still an approximation; cancels leak. +- Cap the win at exit — you don't know `losers_sum` at exit time; deferral removes that. +- Full-AMM settlement — abandons the pari-mutuel thesis of VIZ. +The deferred outcome-contingent claim is the only option giving a **hard** LP guarantee while +staying pari-mutuel. diff --git a/docs/prediction-markets/guides/active-lp.md b/docs/prediction-markets/guides/active-lp.md new file mode 100644 index 0000000000..7cb73f7a7d --- /dev/null +++ b/docs/prediction-markets/guides/active-lp.md @@ -0,0 +1,42 @@ +--- +title: "Active LP — direct market liquidity" +description: "A direct LP sets the curve depth of a specific market, earns from fees and late-bet penalties, and is principal-protected: withdrawal is price-neutral, and settle_liquidity returns the principal unconditionally. Leverage does not touch the direct LP." +--- + +# Active LP: direct market liquidity + +You give a market depth: you put VIZ into the curve of a specific market so that bets do not move the price too sharply. In return you receive a share of the fees. The key difference from a "market-maker-as-banker" model: on VIZ, direct liquidity is **principal-protected** — the outcome cannot put you in the red. + +## The gist in two paragraphs + +Your capital (`pm_add_liquidity`) enters the market's curve and determines how smoothly bets move the price. The deeper the pool, the more comfortable large players are and the larger the volume — and therefore the fees. You earn a **percentage of bets** (the market fee) plus a share of **late-bet penalties** (the anti-sniping penalty), which accrue in favor of LPs. + +Your principal does not depend on who won. Withdrawal (`pm_withdraw_liquidity`) is **price-neutral**: a proportional shrink of the reserves returns your principal without moving the curve (a round trip does not change the price). And at market settlement, `settle_liquidity` returns each LP's principal **unconditionally**, plus a bonus (fees, remainders undistributed to winners, the penalty pool). LP = principal-protected + fee income — and the fee income is realized **only** at settlement. + +## What happens, step by step + +**You add liquidity.** `pm_add_liquidity` — VIZ goes into the market's curve and you receive a pool share proportional to your contribution. Your deposit does not shift the price (you add symmetrically). The minimum contribution is `pm_min_liquidity` (a governed parameter, 100 VIZ by default): **the same floor as creating a market**, and it applies to topping up an open market as well. The reason is technical — every call creates a SEPARATE liquidity row (contributions are not merged into one position) and settlement walks all of them, so cheap micro-deposits cannot be allowed. If you want to add less, add less often and in larger amounts. + +**While the market is open.** A fee is withheld from bets and distributed to LPs by share. Late bets pay the anti-sniping penalty, which also goes in favor of liquidity. Your income accrues as turnover grows. + +**You withdraw (optionally).** `pm_withdraw_liquidity`, partially or fully. The withdrawal is price-neutral: you get back your **principal** (fee income is realized only at settlement — see below), and the curve does not shift. No impermanent loss as in classic AMMs: an in-and-out round trip neither moves the price nor eats your capital. + +**Settlement.** Once the market is resolved, liquidity returns **on its own**, through per-block settlement. `settle_liquidity` hands each LP the principal unconditionally plus a bonus. Liquidity is locked from the close of betting until settlement (while payouts are computed), then released. + +## What an active LP needs to understand + +- **Principal-protected refers to the outcome, not to everything under the sun.** You do not lose principal because of who won the market. Your income is fees and penalties; outcome risk is not shifted onto you. +- **You are not the counterparty to leverage.** Leverage traders' loans are fronted by the **lazy pool** (a passive product), not by your direct liquidity. Leverage does not touch your principal. (There is a subtle design nuance about covering leverage overprofit — it is localized to the pool, not to direct LPs.) +- **Depth = volume = income.** Thin liquidity scares off large bettors; your contribution directly affects market quality and, through volume, your fees. +- **The lock during settlement is normal.** From the close of betting until payouts are computed, liquidity is locked; that is the settlement procedure, not a loss. Afterwards the principal returns. +- **Income is realized at settlement.** Earned fees (earned_fee) are finalized when the market resolves; before resolution they are reflected, but they are booked in the return. +- **Withdrawing early means principal only.** `pm_withdraw_liquidity` requires the row to be active (`status == 0`), and the fee counter is only assigned together with `status = 3` at settlement — so an early withdrawal cannot observe any commission; you simply get your principal back, price-neutrally. There is no separate "income so far" to collect mid-flight. +- **Void markets pay no income.** If a market ends void (no contest — the oracle produced no winner, e.g. the source canceled or was 50/50), bets are refunded and LPs get their principal back with **zero** bonus. LP income comes only from markets that resolve to a winner. + +## Roles next to yours + +- **Market creator** — provides the initial liquidity (they are a direct LP too) and sets the fee. +- **Bettor** — pays the fee that makes up your income. +- **Passive LP (lazy pool)** — a different product: passive capital that fronts leverage; not to be confused with direct market liquidity. + +Further reading: "Passive LP (lazy pool)" (how it differs and where the leverage risk sits), "Why a pool, not odds" (how the curve works), "Market creator" (how the fee and depth are set). diff --git a/docs/prediction-markets/guides/bettor.md b/docs/prediction-markets/guides/bettor.md new file mode 100644 index 0000000000..d10ba6bc56 --- /dev/null +++ b/docs/prediction-markets/guides/bettor.md @@ -0,0 +1,42 @@ +--- +title: "Bettor — how to place bets on VIZ prediction markets" +description: "A bet goes into the shared market pool and moves the floating odds; the prize is split among those who called it right, in proportion to their shares, and the payout is automatic on resolution." +--- + +# Bettor: how to place bets on VIZ prediction markets + +You came to bet on an outcome — "yes/no", "who wins", "will the event happen". On VIZ this does not work like a bookmaker with fixed odds, but as a **shared pool**. Let's go through what exactly happens to your money and how the winnings are formed. + +## The gist in two paragraphs + +You stake VIZ on one of the outcomes. Your bet goes into the **market pool** and moves the price: the more has been staked on an outcome, the more expensive it becomes and the fewer "shares" (weight) you get for the next token. The odds are not locked in at the moment of the bet — they are **floating**, set by the balance of the pool right now. + +When the oracle announces the outcome, the entire pool is split among those who called it right, in proportion to their shares. Lose, and you lose your bet — it goes to the winners. Win, and you take your share of the prize pool. No "locked in 2.5, now wait": the odds are what you see as the current price, and they change as others bet. + +## What happens step by step + +**The bet.** You choose an outcome and an amount. The node computes along the pool curve how many shares (weight) you are due for that amount at the current price, and records the position. The amount goes into the pool, and the outcome's price shifts up. + +**While the market is open.** The price is alive: others bet, the odds float. You can bet more, and on markets where cancellation is allowed you can **cancel** a bet before it closes (`pm_cancel_bet`). Important: cancelling sells your position back along the current curve rather than refunding the nominal. If the market has moved since your bet, you get back less than you put in — that is not a penalty but a fair exit price; you never receive more than your bet (and if the price moved in your favour, the surplus along the curve becomes a deferred claim contingent on the outcome, rather than being handed to the market). Details — "[Cancelling a bet](./cancel-bet)". + +**Betting closes.** At `betting_expiration` bets are no longer accepted. After that comes waiting for the outcome from the oracle. + +**Resolution.** The oracle announces the winning outcome (`pm_resolve_market`). Payout to the winners is **automatic** and virtual: there is no "claim" button to press, the winnings are credited to your balance on resolution. Losing outcomes are zeroed out, and their money goes into the winners' prize pot. + +**If there is no outcome.** The event was cancelled or the source disappeared — the oracle declares **no-contest** (`pm_no_contest`): bets are refunded, nobody wins and nobody loses. And if the oracle stayed silent right up to the deadline, the market is voided as **missed-resolution** — your bets are refunded all the same (and the oracle is penalised for it). Either way, your money never "burns". + +## What a bettor needs to understand + +- **The odds are not fixed.** The price you see is the state of the pool right now. An early bet on an unpopular outcome buys more shares (you entered cheaper); once the crowd arrives, the price is different. Details — in the article "Why a pool and not odds". +- **The payout is proportional to shares, not "bet × odds".** You split the prize pot with the other winners by position weight. The final multiplier depends on how the bets ended up distributed across outcomes. +- **No manual claiming.** Winnings and refunds arrive automatically on resolution/cancellation. +- **You can challenge the oracle.** If the outcome was announced incorrectly, it can be disputed within the dispute window (`pm_dispute_create`) — see the article on disputes. +- **Liquid balance.** A bet requires free VIZ; staked SHARES do not count. The wallet will tell you if you are short. + +## Roles next to you + +- **Oracle** — the one who announces the outcome and answers for it with reputation and an insurance bond. +- **Liquidity provider** — the one whose capital sets the depth of the curve (so your bet does not move the price too sharply). +- **Leverage trader** — bets on the **price** with a loan from the pool rather than on the outcome; a separate instrument. + +Where to go next, to taste: "Why a pool and not odds" (price mechanics), "Oracle and resolution" (who announces the outcome and how), "Disputes" (how to challenge it). diff --git a/docs/prediction-markets/guides/cancel-bet.md b/docs/prediction-markets/guides/cancel-bet.md new file mode 100644 index 0000000000..ebb399ecb2 --- /dev/null +++ b/docs/prediction-markets/guides/cancel-bet.md @@ -0,0 +1,37 @@ +--- +title: "Bet cancellation — exit at the curve price" +description: "pm_cancel_bet sells the position back at the current curve (curve-priced) instead of refunding face value. The amount you receive is never more than your stake (cap-at-stake): a losing difference goes into the market's forfeit_pool, while a profitable excess becomes an outcome-contingent deferred claim. Cancellation is possible before betting closes, if the market allows it." +--- + +# Bet cancellation: exit at the curve price + +Changed your mind before the market closes? On markets where it is allowed, a bet can be cancelled (`pm_cancel_bet`). But it is important to understand: this is **not** "give me my money back", it is selling the position back at the current price. Let's go through how much comes back and why. + +## The gist in two paragraphs + +A cancellation is an exit at the **current curve** (curve-priced), not a refund of face value. You sell your shares (weight) at the price that has formed by the moment of cancellation. If the market has shifted since your bet, you get back **less** than you put in — that is a fair exit price, not a penalty. And conversely: what you receive is **never more than your stake** (cap-at-stake) — curve profit cannot be cashed out at the moment of cancellation. + +What happens to the difference depends on its direction. If the curve pays out **less** than the stake (the price moved against you) → the shortfall (`curve_residual`) **stays with the market**, goes into the `forfeit_pool` and is distributed to winners at settlement. If it comes out **higher** than the stake (the price moved in your favor) → the excess is neither given to the market nor paid immediately: it becomes your **outcome-contingent deferred claim** and is settled at settlement if your outcome won (details — "[Early exit and the deferred claim](./early-exit)"). No tokens are minted or burned in the process. You can cancel only before betting closes (`betting_expiration`) and only if the market was created with cancellation allowed. + +## How it works + +**Selling at the curve.** The node computes what your shares are worth at the pool's current reserves and returns that amount. The price is the same curve that every bet has moved; your exit nudges it slightly back as well. + +**Cap-at-stake.** The immediate return is capped above by your original stake. Even if your outcome's price has risen and "on the curve" the shares are worth more, you will get back no more than you put in. The curve excess is not lost: it is carried over into a **deferred claim** and settled at resolution if your outcome won. A cancellation is an exit, not a way to take profit instantly. + +**Residual → forfeit_pool.** If the curve pays out less than face value, the difference (`curve_residual`) is not lost into nowhere: it is routed into the market's `forfeit_pool` and goes to winners at settlement. Tokens are preserved — this is part of the construction that maintains conservation (no "burning" and no silent minting). + +## What you need to understand + +- **The return ≠ face value.** If the market has shifted since your bet, you get back less. That is the price of liquidity, not a punishment. +- **You cannot profit on a cancellation instantly.** Cap-at-stake: the most you receive is your own stake. Curve profit becomes a deferred claim and arrives at resolution, if your outcome won. +- **A losing difference stays with the market.** If the exit pays less than the stake, the shortfall goes to winners through the forfeit_pool, not "into thin air". A profitable excess, conversely, is locked in for you as a deferred claim. +- **Only before the close and only where allowed.** After `betting_expiration` there is no cancellation; on markets without the cancellation flag — likewise. +- **Leverage closes differently.** Exiting a leverage position (`pm_leverage_close`) has its own mechanics (loan, cancel_value); there the residual is also routed into the forfeit_pool. See [Leverage trader](./leverage-trader). + +## Related + +- [Bettor](./bettor) — the life cycle of a bet, where cancellation is one of the paths. +- [Early exit and the deferred claim](./early-exit) — where the profitable tail of a cancellation goes. +- [Why a pool, not odds](./why-pool-not-odds) — why the price (and the exit price) floats. +- [Specification](../specification) — the curve-priced cancel formula and the residual route. diff --git a/docs/prediction-markets/guides/commit-reveal.md b/docs/prediction-markets/guides/commit-reveal.md new file mode 100644 index 0000000000..85b38eb76f --- /dev/null +++ b/docs/prediction-markets/guides/commit-reveal.md @@ -0,0 +1,44 @@ +--- +title: "Hidden bets — commit-reveal and batches" +description: "A hidden bet: first the commit (escrow without revealing the outcome/amount), then the reveal inside a window. Privacy until the reveal protects you from copying and sniping. Fail to reveal and part of the escrow is forfeited into the forfeit_pool. Requires a market with allow_batch." +--- + +# Hidden bets: commit-reveal and batches + +Sometimes it matters that your bet is **not visible in advance** — so that it cannot be copied or played against. That is what the two-phase hidden bet is for: first you "seal" it (commit), then you reveal it (reveal). Let's go through why this exists and what happens if you do not reveal. + +## The gist in two paragraphs + +An ordinary bet is visible on chain immediately — outcome, amount, time. A hidden bet conceals that until the reveal: you put up **escrow** and commit the bet (`pm_commit_bet`) without showing which outcome and how much. Later, inside the **reveal window**, you reveal it (`pm_reveal_bet`) and it enters the pool like any other bet. Until the reveal nobody knows your position — that is protection against copying, front-running and sniping a large bet. + +Privacy comes with responsibility: if you **fail to reveal** the bet inside the window, a **forfeit** kicks in — part of the escrow (the penalty) goes into the market's `forfeit_pool` (to the winners), the rest is refunded. Hidden bets only work on markets where batch mode is enabled (`allow_batch`) and require a minimum escrow (`pm_min_batch_bet`). + +## How it works step by step + +**Commit.** `pm_commit_bet`: you put up escrow (≥ `pm_min_batch_bet`, on the order of 1 VIZ) and commit the bet in sealed form. The chain shows that you staked something, but not the outcome or the amount. The escrow is debited. + +**The reveal window.** You get a limited window (the epoch + `pm_reveal_window_blocks`, on the order of minutes). You must reveal within it. + +**Reveal.** `pm_reveal_bet`: you disclose the outcome and the amount, the bet enters the pool at the current price — from there on it is an ordinary bet. To reveal, the client needs the `commit_id` of your commit (`get_account_commits`). + +**No reveal → forfeit.** Miss the window and the forfeit is automatic: the penalty (a fraction of the escrow, set by `no_reveal_fee_percent`) goes into the market's `forfeit_pool`, the remainder is refunded. That is the price for taking a slot and not completing the bet. + +## Why this exists + +- **Privacy of intent.** A large player does not want their bet copied or played against before it enters the pool. +- **Anti-sniping.** The hidden phase makes it harder to peek at and front-run other people's bets. +- **Batches.** Commits are collected and revealed in batches — a mode for a fairer and more private round of betting. + +## What you need to understand + +- **Two phases, two actions.** Commit and reveal are different operations, with a window in between. Forget to reveal and you lose the penalty. +- **Revealing is mandatory.** The forfeit is not a bug but an incentive to finish what you started; the penalty goes to the winners through the forfeit_pool. +- **Only on allow_batch markets.** Not every market supports hidden bets. +- **You need the commit_id.** To reveal, the client fetches your open commits (`get_account_commits`) — without it there is nothing to reveal. +- **Kill switch.** The whole of commit-reveal is a subsystem with a median-voted toggle (`pm_commit_reveal_enabled`); validators can disable it without a hard fork. + +## Related + +- [Bettor](./bettor) — the ordinary (visible) bet, for comparison. +- [Why a pool and not odds](./why-pool-not-odds) — where a revealed bet lands. +- [Specification](../specification) — reveal windows, `no_reveal_fee_percent`, batch mechanics. diff --git a/docs/prediction-markets/guides/coupon.md b/docs/prediction-markets/guides/coupon.md new file mode 100644 index 0000000000..f14e9df62e --- /dev/null +++ b/docs/prediction-markets/guides/coupon.md @@ -0,0 +1,40 @@ +--- +title: "Coupon — several bets in one transaction" +description: "A coupon packs N bets (pm_place_bet) into a single signed VIZ transaction. The transaction is atomic: any invalid leg rejects the whole coupon, there is no half-way state. This is not a parlay — the legs are independent and the payouts are not multiplied together." +--- + +# Coupon: several bets in one transaction + +A coupon is a way to bet on several outcomes at once without signing each bet separately. You collect lines by tapping outcomes, set the amounts and send everything in **one transaction**. + +## The gist in two paragraphs + +Technically a coupon is N `pm_place_bet` operations inside one signed VIZ transaction. A transaction on chain is **atomic on write**: it is either applied in full or rejected in full. So if even one leg is invalid — the market has already closed, the balance does not cover the sum of all legs, the slippage protection kicked in — **the whole coupon fails**. There is no such state as "half the bets went through", and you never have to untangle a partial result. + +At the same time a coupon is **not a parlay (not an accumulator)**. Each leg is an ordinary standalone bet into the pool of its own market, with its own payout. The odds are not multiplied together: the winnings on one leg do not depend on whether the others came in. A coupon saves signatures and makes a set of bets simultaneous — but it does not create an "all outcomes must hit" bundle. A real bundle with multiplication is a separate protocol primitive, and at the bet level it does not exist yet. + +## How it works + +**Collecting.** In the event feed and on the event page (the "Lines" view) a tap on an outcome adds a leg to the coupon: market, outcome, amount (1 Ƶ by default) and your slippage protection. Tapping the same outcome again updates the leg instead of creating duplicates. The coupon lives locally in the browser — until it is sent it does not touch the chain in any way. + +**Checks before sending.** A coupon assembled yesterday may still carry a market whose betting has already closed. The chain will reject such a leg — and the whole transaction with it, so the client checks every leg when the coupon is opened, using the same rule as the node (the market is active and either open-ended or its deadline has not passed yet), marks the dead ones and does not allow sending until they are removed. The liquid balance is checked separately, against the **sum of all legs**: the pool only accepts bets in free VIZ, staked shares do not count. + +**Sending.** All legs are signed with one key (active) and go out as one transaction. There is no network fee — the limit is set by the account's energy, and by that measure a transaction of N bets is cheaper than N separate ones. + +**What happens next.** Once written, each leg lives its own life: its own share in the pool of its own market, its own resolution, its own automatic payout. In "My activity" they show up as ordinary bets. + +## What you need to understand + +- **All or nothing — on write, not on the outcome.** The coupon's atomicity is about landing in a block, not about calling it right. One leg lost — the others still count and still pay. +- **Payouts are not multiplied together.** This is not a parlay. If you want more risk, increase the amount of a leg, not the number of legs. +- **One dead leg breaks the submission.** A closed market, not enough free VIZ for the sum of all legs, a triggered minimum-shares protection — and the chain rejects the entire coupon. The client highlights such legs in advance. +- **Hidden bets do not go into a coupon.** Commit-reveal and batch mode are separate paths with their own reveal window; only ordinary ("instant") bets are collected into a coupon. +- **The coupon is stored in the browser.** Until you send it, it is a draft on your device: the chain knows nothing about it, and it will not be there on another device. + +## Related + +- [Bettor](./bettor) — the life cycle of the single bet that a coupon is made of. +- [Why a pool and not odds](./why-pool-not-odds) — why the price of a leg floats and what the minimum-shares protection is for. +- [Events and metadata](./events-metadata) — how the lines of one match are assembled into an event card, which is a convenient place to build a coupon from. +- [Hidden bets (commit-reveal)](./commit-reveal) — the path that is not part of a coupon, and why. +- [`pm_*` operations](../../protocol/operations/prediction-markets) — the `pm_place_bet` operations the transaction is built from. diff --git a/docs/prediction-markets/guides/disputes.md b/docs/prediction-markets/guides/disputes.md new file mode 100644 index 0000000000..ac40111bda --- /dev/null +++ b/docs/prediction-markets/guides/disputes.md @@ -0,0 +1,44 @@ +--- +title: "Disputes — how to challenge an outcome" +description: "If the oracle announced an incorrect outcome, it can be challenged within the grace window (pm_dispute_create, bond fee). A vote resolves the dispute (committee or account mode); an incorrect resolution penalises the oracle, and the disputer risks their bond. Payouts may be recomputed." +--- + +# Disputes: how to challenge an outcome + +An oracle is a person or a service, and it can make a mistake or cheat. A dispute is the safety catch: the mechanism participants use to challenge an announced outcome while the payouts can still be recomputed. Let's go through how it works and what each side risks. + +## The gist in two paragraphs + +Once the oracle has announced the outcome (`pm_resolve_market`), a **dispute window** opens (the grace period, `pm_dispute_grace_sec`, on the order of 12 hours). Within that window any participant can file a dispute (`pm_dispute_create`) by putting up a **bond fee** (around 1000 VIZ) — that is the price of having the network review the result, and the protection against spam from empty challenges. + +The dispute is resolved by a **vote** (`pm_dispute_vote`, with the active key) — in one of the modes set by the market creator: committee (trusted arbiters) or account voting. If the dispute finds the resolution incorrect, the outcome is corrected, payouts are recomputed, **the oracle is penalised** (bond/reputation), and the disputer gets a reward. If the resolution is found correct, the disputer loses the bond. There is money at stake on both sides, which is why challenges are filed on the merits and not at random. + +## How it works step by step + +**Resolution and the start of the window.** The oracle announced the outcome → payouts were credited → the dispute grace timer started. While it runs, the result is not final. + +**Filing a dispute.** `pm_dispute_create` with the bond fee. The market moves into dispute status (the payout is deferred/flagged) and a dispute record appears naming the oracle and the initiator. The `pm_dispute_opened` vop is emitted (it lands in the history of both the oracle and the disputer). + +**Voting.** Participants of the mode vote with `pm_dispute_vote` (active key). The mode and its membership are set at market creation (committee or accounts). Votes are collected until the response/decision deadline. + +**Finalisation.** The dispute is closed (`pm_dispute_finalize`) according to the vote, or auto-closed (`pm_dispute_auto_close`) if the oracle did not respond in time. The result: +- **Resolution incorrect** → the outcome is changed, payouts are recomputed for the correct outcome, the oracle is penalised, and the disputer is rewarded. +- **Resolution correct** → the result stands, the disputer loses the bond, and the oracle's reputation is confirmed. + +**Effect on payouts.** While the dispute runs, the final payouts on the challenged market are not considered final. After finalisation, settlement proceeds on the confirmed outcome. + +## What to understand + +- **The window is limited.** A challenge is only possible during the grace period after the announcement. Miss it and the outcome is final. On an early resolution (`allow_early_resolution`) the window collapses to the moment of resolution, but disputers still keep the full `pm_dispute_grace_sec` from the announcement. +- **The bond cuts both ways.** The disputer risks the fee, the oracle risks the bond and reputation. That makes a dispute expensive for lying and cheap for telling the truth. +- **The creator sets the mode.** Committee mode is faster and more predictable (trusted arbiters); account mode is more decentralised. Check a market's mode before a large bet. +- **The vote uses the active key.** As do most participant PM operations (`pm_dispute_vote`, `pm_dispute_create`). +- **A dispute is a last resort.** For an honest oracle with a good `reliability_score` disputes are rare; systematically losing them drags down its reputation and insurance. + +## Roles nearby + +- [Oracle](./oracle) — the one whose resolution is being challenged; carries the bond and the reputation. +- [Bettor](./bettor) — can initiate a dispute if the outcome was announced incorrectly. +- [Market creator](./market-creator) — sets the dispute mode and window at creation. + +More on the topic: [Oracle](./oracle) (what it is penalised for and how reputation is computed), [Bettor](./bettor) (the life cycle of a bet), [Specification](../specification) (the formal dispute rules and deadlines). diff --git a/docs/prediction-markets/guides/early-exit.md b/docs/prediction-markets/guides/early-exit.md new file mode 100644 index 0000000000..3b774a53f0 --- /dev/null +++ b/docs/prediction-markets/guides/early-exit.md @@ -0,0 +1,43 @@ +--- +title: "Early exit and the deferred claim" +description: "Profit from an early exit (bet cancellation or a curve-priced leverage close) is not paid out immediately: it is booked as a deferred outcome-contingent claim and settled from a bounded slice of the losing pool. The principal comes back right away, and the LP stays protected." +--- + +# Early exit and the deferred claim + +A VIZ market is a hybrid: entry and early exit go through the **curve** (as on an AMM), while positions held to the end are settled **parimutuel** (a common pool). Because of this, "profit from an early exit" is arranged more subtly than it looks. Let's go through why winnings from an early exit do not arrive immediately. + +## The gist in two paragraphs + +When you exit early — cancel a bet (`pm_cancel_bet`) or close leverage (`pm_leverage_close`) — the curve price of your position may turn out to be **higher** than your stake. But that profit cannot be paid out right now: it is not backed by losers (the market is not resolved yet), and if it were paid out of the curve, the shortfall would fall on liquidity providers — and on VIZ an LP is **principal-protected**. That is why the profitable "tail" of an early exit is not cashed out on the spot. + +Instead, the system returns your **principal immediately and unconditionally** (your own money, never more than the stake) and books the profitable tail as an **outcome-contingent deferred claim**. It is settled **at settlement** from a **bounded slice of the losing pool** — that is, only if your chosen outcome won and there is something in the pot to pay from. This keeps token conservation intact and prevents LPs from subsidizing traders. + +## How it works + +**The principal — immediately.** A cancellation/close returns `min(curve_price, your_stake)` at once. An early exit can cut a loss or get you out flat, but it does **not** realize profit at the moment of exit. + +**The profitable tail — into a deferred claim.** The difference `max(curve_price − stake, 0)` is booked as a claim on **the outcome you chose**. Not cash, but "if this outcome wins, we top you up at settlement". + +**Settled from a bounded pool.** At resolution the claim is paid from a **bounded slice of the losing pool** (not from the curve, not from LP principal). If there is no loser money behind it, the payout is trimmed. No silent minting. + +**A fair entry price (protection against gaming).** The "principal/tail" split is recomputed against the curve depth **at the moment of your bet**, not the current one. That closes the trick of "place a bet → add liquidity yourself → inflate the depth → withdraw a bigger tail": normalization can only decrease the payout, never increase it. + +## Why it works this way + +- **LPs are promised principal protection.** If early-exit profit were paid from the curve, the shortfall (when profit outruns losing stakes) would be written off against LP principal or minted — both break the guarantee. The deferred claim moves the payout to settlement, where the source is losers, not LPs. +- **Parimutuel is backed only by losers.** A winner's profit is someone else's loss. Before resolution there are no "losers" yet — so there is nothing to pay the profit from right away. + +## What you need to understand + +- **On an early exit you take back principal, not profit.** The profitable tail waits for resolution. +- **The tail is contingent on the outcome.** If your chosen outcome loses, there is no tail; if it wins, the tail is settled from the pot (within what is available). +- **This protects LPs and conservation.** The mechanism is deliberate, not a wallet limitation. +- **Leverage follows the same principle.** Leverage profit also arrives after resolution, from a bounded pool. See [Leverage trader](./leverage-trader). + +## Related + +- [Bet cancellation](./cancel-bet) — how the return on an early exit is computed. +- [Leverage trader](./leverage-trader) — why leverage profit is deferred. +- [Active LP](./active-lp) — whose protection this provides. +- [Specification](../specification) — the formal model of the deferred claim and the bounded slice. diff --git a/docs/prediction-markets/guides/events-metadata.md b/docs/prediction-markets/guides/events-metadata.md new file mode 100644 index 0000000000..5c8e947836 --- /dev/null +++ b/docs/prediction-markets/guides/events-metadata.md @@ -0,0 +1,107 @@ +--- +title: "Events and metadata — how the client assembles markets into matches" +description: "How Forecaster aggregates markets into \"Events\": the event / event_title / child metadata keys, the parent and the child markets, what the node indexes and how to create a market the client will recognise as an event." +--- + +# Events and metadata: how markets are assembled into matches + +A single real-world match is usually several markets: "who wins", "total kills", "first Roshan". +On chain these are independent `pm_market_object`s, but the client (Forecaster) shows them as one event +card with bets on outcomes and a "N more lines" link. The grouping happens **through metadata only** — +there are no special "parent" objects in consensus, and that is deliberate: the protocol stays +minimal, while the grouping is set by the market creator at creation time. + +## The short model + +- Every market carries a free-form text field `metadata` (a JSON string in `pm_create_market`). +- The node parses a **whitelist of keys** out of it and builds indexes; everything else is ignored. +- Markets with the same `event` are "one event". A market without `child` is the front (parent) one, + a market with `child: 1` is a child line (a prop). +- Forecaster: the "Events" tab groups active markets by `event`, shows the winner-line market as the + front one, hides the children from the general feeds and reveals them on the event page. + +## The metadata keys the node indexes + +The node extracts only these fields from `metadata` (other keys are not indexed, but remain in the +raw JSON — clients can read them themselves): + +| Key | Type | What for | +|------|-----|-------| +| `title` | string | The human-readable question of the market (the card headline). | +| `category` | string | The listing section (`esports`, `sports`, `crypto`…) — the `by_category` index. | +| `subcategory` | string | A refinement of the section (optional). | +| `tags` | array or CSV | Tags for filters; the client reads the **array** `market.metadata.tags`, which the node rebuilds itself. | +| `image` | string (URL) | The card cover (a link, not hosted on chain). | +| `description` | string | Short resolution rules — "how the oracle will decide the outcome". | +| `event` | string (slug) | **The event grouping key.** All markets of one match set the same `event`. | +| `event_title` | string | The human-readable name of the event ("Dota 2: MOUZ vs Vici — TI 2026"). | +| `child` | 1 / true | **A child line (a prop).** Hidden from the category/tag feeds; visible on the event page. | +| `banned_jurisdictions` | array or CSV | The jurisdiction filter for clients. | +| `condition_id` | string | The source's dedup identifier (for mirroring parsers). | + +Parsing rules: `metadata` must be a valid JSON object (non-JSON is simply not indexed); +`tags`/`banned_jurisdictions` are accepted both as an array and as a CSV string; `child` is accepted as +`true`, `1` or `"1"`. Tags are matched case-insensitively. + +## How to designate the "parent" market + +The parent is not designated explicitly — it is **derived from the absence of `child`**: + +1. Give all the markets of the match the same `event` (a stable slug: latin letters, hyphens — + for example `dota2-mouz-vg-2026-07-12`) and the same `event_title`. +2. For the main market of the match ("who wins" / moneyline) — **do not set** `child`. That is the + parent: it stays visible in all feeds and becomes the face of the event card. +3. For all the other lines (totals, handicaps, special markets) — `child: 1`. They disappear from the + general feeds (no noise in the categories), but remain fully available on the event page and by direct link. + +A minimal `metadata` example for three markets of one match: + +```json +// Parent (winner line) — WITHOUT child +{"title":"Will MOUZ beat Vici Gaming?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming"} + +// Child line 1 +{"title":"Total kills over 45.5 (map 1)?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming","child":1} + +// Child line 2 +{"title":"First Roshan — MOUZ?","category":"esports","tags":["dota-2"], + "event":"dota2-mouz-vg-2026-07-12","event_title":"Dota 2: MOUZ vs Vici Gaming","child":1} +``` + +Important: `event` is **immutable in practice** — clients group by an exact string match, so pick the +key before creating the markets and use it identically across all the lines of the match +(case and hyphens matter). + +## What the node does + +- Builds the market's meta object (`pm_market_meta`) with the parsed fields and indexes: by category, + by tags and **by event** (`by_meta_event`); the meta goes into the snapshot. +- `list_markets_by_category(...)` **hides children** by default (`hide_children = true`, + the 8th argument) — the feeds show only parents; pass `false` to see all the lines. +- `list_markets_by_event(event, from, limit)` returns **all** the markets of the event — the parent and + the children, with no filter. This is the API of the event page. +- In listing rows `event_title` is returned at the top level, in the full market card — inside + `metadata`; `tags` are rebuilt into an array by the node. + +## What Forecaster does with it + +- **The "Events" tab** (the sports-book view): active markets are grouped by `event`; the front market + is the one whose headline looks like a winner line (`winner` / `moneyline` / `to win`), otherwise the + first binary one; the card shows the outcomes with the current odds (a tap adds a leg to the coupon) and + a "N more lines" link. +- **The event page** `#/event/` — all the lines of the match in one list (`list_markets_by_event`). +- **Cards** show `event_title` above the question; the "Category › tags" breadcrumbs lead to the listings. +- **Category/tag feeds** do not show child markets — the props live behind the event card. + +The practical takeaway for a creator: correctly set `event`/`event_title`/`child` is the difference +between "ten scattered markets making noise in the feed" and "one tidy match card with all the lines +inside". Get the event key wrong and the lines will not group; forget `child` and the props will +clutter the general feeds. + +## See also + +- [Market creator](./market-creator) — starting liquidity, the oracle, the fee. +- [Multi-outcome markets](./multi-outcome) — when one LMSR market beats several binary ones. +- [Specification](../specification) — the formal model of objects and indexes. diff --git a/docs/prediction-markets/guides/index.md b/docs/prediction-markets/guides/index.md new file mode 100644 index 0000000000..ec32d6f50f --- /dev/null +++ b/docs/prediction-markets/guides/index.md @@ -0,0 +1,45 @@ +--- +title: "Prediction market guides — by role and by feature" +description: "Explainer articles for participants of VIZ (Onix) prediction markets: every role (bettor, market creator, oracle, LP, leverage trader) and every mechanic in plain language, with links to the formal specification." +--- + +# Prediction market guides (Onix) + +Explainer articles for **participants** — not a specification, but "how this works for you". Every role +is explained from the participant's point of view: a short model → step by step → what matters to +understand → links to neighbouring articles. Formal mechanics and parameters are in the +[Specification](../specification), operations are in +[Operations](../../protocol/operations/prediction-markets). + +## By role — "you are the participant" + +| Role | What it covers | +|------|-------| +| [Bettor](./bettor) | I place a bet. Floating odds, payout proportional to shares, automatic payout on resolution. | +| [Market creator](./market-creator) | I open a question. Starting liquidity, fee, choice of oracle and dispute mode. | +| [Oracle](./oracle) | I announce the outcome. Insurance and risk floor, penalties for getting it wrong, `reliability_score` reputation. | +| [Active LP](./active-lp) | I provide depth for the market curve. Income from fees, principal-protected, price-neutral withdrawal. | +| [Passive LP (lazy pool)](./passive-lp) | I deposit VIZ passively. Income from leverage and fees, shares by equity, FIFO withdrawal. | +| [Leverage trader](./leverage-trader) | I bet on the price with a loan from the pool. Markup + funding, liquidation, force-close when betting closes. | + +## By feature — "how the mechanism is built" + +| Feature | What it covers | +|------|-------| +| [Why a pool and not odds](./why-pool-not-odds) | A floating price along a curve (CPMM/LMSR) instead of fixed odds. | +| [Oracle and resolution](./resolution) | The resolution timeline: deadlines, early resolution, missed resolution, no-contest. | +| [Disputes](./disputes) | How to challenge an outcome: the grace window, the bond, voting modes, reward/penalty. | +| [Multi-outcome markets](./multi-outcome) | One market for many outcomes, LMSR vs binary CPMM. | +| [Cancelling a bet](./cancel-bet) | Curve-priced exit, cap on the stake, the difference in `forfeit_pool`. | +| [The lazy pool in detail](./lazy-pool) | Shares at the equity price, sources of income, FIFO withdrawal, the free ≥ 0 invariant. | +| [Early exit and deferred claim](./early-exit) | The profit of an early exit is a deferred claim, paid out of the pot at settlement. | +| [Hidden bets (commit-reveal)](./commit-reveal) | Privacy until the reveal, the reveal window, forfeit for not revealing. | +| [Events and metadata](./events-metadata) | How markets are assembled into a match card: event / event_title / child, the parent and the child lines, what the node indexes. | +| [Coupon](./coupon) | Several bets in one transaction: atomicity on write, why this is not a parlay, what breaks the submission. | + +## Where to go next + +- [Specification](../specification) — formal rules, parameters, object model. +- [Whitepaper](../whitepaper) — the thesis: why liquidity without risk, two types of markets, the flywheel. +- [`pm_*` operations](../../protocol/operations/prediction-markets) — signed consensus operations. +- [Workflows](../workflows) — a single market carried through all the roles. diff --git a/docs/prediction-markets/guides/lazy-pool.md b/docs/prediction-markets/guides/lazy-pool.md new file mode 100644 index 0000000000..53d5a262aa --- /dev/null +++ b/docs/prediction-markets/guides/lazy-pool.md @@ -0,0 +1,58 @@ +--- +title: "The lazy pool in detail — shares, equity price, FIFO withdrawal" +description: "Lazy pool mechanics: shares are minted at the equity price (free+allocated−pending_withdrawals), yield comes from leverage markup + funding + fees, withdrawals go through a FIFO queue with the free_balance ≥ 0 invariant, and the emergency-withdrawal penalty applies only to rewards." +--- + +# The lazy pool in detail: shares, equity price, withdrawal + +This is a deep dive into the mechanics of the lazy pool. If you need the "why, and is this product for me" overview — start with the [Passive LP](./passive-lp) role; here we cover exactly how shares, yield and withdrawals are computed. + +## Shares at the equity price + +When you deposit (`pm_lazy_deposit`), the pool mints you **shares** not at a 1:1 face value, but at the current **equity price**: + +> equity = free_balance + allocated − pending_withdrawals + +- **free_balance** — the pool's free VIZ, not deployed right now. +- **allocated** — capital put to work (leverage loans, depth). +- **pending_withdrawals** — withdrawals already promised but not yet paid out (the FIFO queue). + +Share price = equity / total number of shares. Why this way: if you counted `free_balance` alone, a new depositor arriving while capital is deployed would get an inflated weight in rewards (capital at work is not visible in free). The equity price splits yield fairly between old and new LPs. If equity ≤ 0 (an edge case), it falls back to 1:1. + +## Where the yield comes from + +- **Leverage markup.** A leverage loan is fronted by the pool; the fixed markup returns as yield. +- **Funding rate.** While a leverage position is open, funding accrues from it in favor of the pool. +- **Fees.** A share of the system's general fees. + +Yield is reflected in the rising value of your share — nothing to claim or reinvest manually. + +## Withdrawal: FIFO queue and the free ≥ 0 invariant + +A withdrawal (`pm_lazy_withdraw`, partial by shares or full) burns your shares immediately, but the payout depends on the free balance: + +- **Enough free_balance** → the payout is instant, as before. +- **Not enough** (capital sits in open leverage) → a `pm_lazy_withdraw_request` is registered in the **FIFO queue**, and the pool's `pending_withdrawals` field grows. + +Requests are settled **in arrival order** on every event that returns free balance: leverage close/liquidation, conversion, a new deposit. Even when no capital returns, the queue still makes progress every block — the node's per-block cron pays up to `pm_settle_rows_per_block` queued requests, so a waiting withdrawal always inches forward. The hard invariant is **free_balance ≥ 0**: the pool physically cannot pay out more than is free. That is a lesson from the early design, when an emergency withdrawal could drag the balance negative (the pool handed out capital that had not yet returned). + +To read the queue: `get_lazy_withdraw_requests(account)`; the pool state — `get_lazy_pool`; your position — `get_lazy_deposit`. + +## Emergency withdrawal and the penalty + +An emergency withdrawal takes a **penalty from accrued rewards only**; the principal is not cut. That is, you always get back what you put in (possibly through the queue), and haste costs you part of the yield, not capital. + +## What you need to understand + +- **Shares are equity-priced, not face-value.** A fair split of yield; with capital deployed, this is critical. +- **A withdrawal may wait in the queue.** Not a loss — the principal returns as leverage unwinds; immediacy is not guaranteed. +- **free_balance ≥ 0 is the law.** The pool never goes negative; payouts beyond the free balance enter the FIFO queue. +- **The penalty hits rewards, not principal.** An emergency exit costs less in money than in nerves. +- **The pool is the counterparty to leverage.** The nature of the risk differs from a direct market LP (see [Active LP](./active-lp)). + +## Related + +- [Passive LP](./passive-lp) — the role overview (is it for you). +- [Leverage trader](./leverage-trader) — who borrows from the pool and pays the markup. +- [Active LP](./active-lp) — direct market liquidity, for comparison. +- [Specification](../specification) — equity formulas, the withdrawal queue, invariants. diff --git a/docs/prediction-markets/guides/leverage-trader.md b/docs/prediction-markets/guides/leverage-trader.md new file mode 100644 index 0000000000..3b3e569bbb --- /dev/null +++ b/docs/prediction-markets/guides/leverage-trader.md @@ -0,0 +1,45 @@ +--- +title: "Leverage trader — betting on price with a loan from the pool" +description: "Leverage: betting on the movement of an outcome's PRICE with a loan from the lazy pool. Flat markup + funding rate, sweep liquidation, force-close at the betting close (it does not wait for the oracle), cannot be opened when less than 24h remain until betting_expiration. Profit is a deferred claim paid after resolution." +--- + +# Leverage trader: betting on price with a loan from the pool + +Leverage is a separate instrument on top of a market. You are betting not "on the outcome until the end", but **on the movement of the price** of an outcome, and you borrow from the lazy pool to amplify the position. This is closer to margin trading than to an ordinary bet. + +## The gist in two paragraphs + +You post collateral and open a leverage position (`pm_leverage_open`): the system borrows the missing amount from the **lazy pool** and places the enlarged size on the chosen outcome. While the outcome's price moves your way, unrealized profit grows; against you, the position approaches **liquidation**. For the loan you pay the pool a **flat markup (~10%)** and a **funding rate** over time; that is the income of passive LPs. + +Leverage **does not wait for the oracle**: the position is force-closed at the price at the moment betting closes (`betting_expiration`) — leverage settles at the market price, not at the announced outcome. If the price hits the threshold earlier, the position is liquidated by a "sweep". Leverage profit is booked as a **deferred claim** and paid out after the market resolves (from a bounded pool of losers), not instantly. + +## What happens, step by step + +**Opening.** `pm_leverage_open`: you post collateral and set the leverage. The pool issues the loan (`pool.free_balance -= loan`), and the total size is bet on the outcome. You can only open if **at least 24 hours** remain until `betting_expiration` — otherwise the position has no room to live, and the opening is rejected. + +**While open.** The outcome's price drifts with bets. Your way — profit grows; against you — liquidation approaches. The funding rate accrues in favor of the pool. You pay for leverage as long as you hold the position. + +**Closing at will.** `pm_leverage_close` — you exit at the current price: the loan is returned to the pool and you take your share (`cancel_value`). The spread/floor remainder (`curve_residual`) is routed into the market's `forfeit_pool` (it goes to winners at settlement) — money does not "freeze". + +**Liquidation.** If the price reaches the threshold, the position is closed automatically (sweep). The collateral repays the loan to the pool; whatever is left follows the settlement rules. + +**Force-close at the betting close.** If you did not close it yourself, at `betting_expiration` the position is force-closed at that moment's price. Leverage **does not depend on the oracle's resolution**: it is about price, not about "who turns out to be right". + +**Profit payout.** Leverage profit is not instant cash: it is a **deferred claim** settled after the market resolves, out of a bounded pool of losing stakes/forfeits (with a cap on the payout). This way the system does not mint tokens out of thin air. + +## What a leverage trader needs to understand + +- **You bet on price, not on the outcome.** Leverage closes at the price when betting closes, not at the oracle's announcement. You can call the "price" right and never see the outcome — those are different things. +- **The loan comes from the pool, and it is not free.** The flat markup plus the funding rate go to the lazy pool. Holding a position for a long time is expensive; funding works against you over time. +- **Liquidation is real.** A move against you closes the position by force, and the collateral repays the loan. Leverage amplifies both profit and loss. +- **The 24-hour window.** You cannot open leverage if less than a day remains until betting closes — the position needs room to exist and to close correctly. +- **Profit arrives after resolution.** Do not count on instantly withdrawing leverage winnings: they are booked as a deferred claim and settled at market settlement, within the available pool. +- **An instrument for those who understand it.** Leverage is more complex than an ordinary bet; if you want a simple bet on the outcome, see the "Bettor" article. + +## Roles next to yours + +- **Passive LP (lazy pool)** — the one you borrow from; your markup and funding are their income. +- **Bettor** — bets on the outcome without a loan and without liquidation; the simpler path. +- **Oracle** — resolves the market; it affects your leverage indirectly (profit is settled after resolution), but the leverage close is tied to price, not to their verdict. + +Further reading: "Passive LP (lazy pool)" (the other side of your loan), "Leverage — the mechanism" (markup and liquidation formulas), "Early exit and the deferred claim" (how and when leverage profit arrives). diff --git a/docs/prediction-markets/guides/market-creator.md b/docs/prediction-markets/guides/market-creator.md new file mode 100644 index 0000000000..24c96200a8 --- /dev/null +++ b/docs/prediction-markets/guides/market-creator.md @@ -0,0 +1,51 @@ +--- +title: "Market creator — how to open a question on VIZ" +description: "How to create a prediction market: set the question and outcomes, put up initial liquidity, choose an oracle and a dispute mode, set the fee. Liquidity is principal-protected and comes back on its own at settlement." +--- + +# Market creator: how to open a question + +You formulate the question people will bet on: "who wins", "will the event happen by a date", "yes/no". Any account can create a market on VIZ — it is an ordinary signed `pm_create_market` operation, with no permissions and no moderation. Let's go through what you set and what you answer for. + +## The gist in two paragraphs + +A market is a question + a set of outcomes + a **liquidity curve** that the price is computed along. You put up the initial liquidity (your own stake), set the market fee, choose an **oracle** (who announces the outcome) and a **dispute mode** (how the outcome can be challenged). Once created, the market lives on its own: people bet, the price floats, at the deadline the oracle announces the result, and the node computes the payouts automatically. + +Your initial stake does not "burn" — direct market liquidity is **principal-protected**: it sets the depth of the curve, earns from fees and is **returned to you in full** at settlement (plus your accrued share of fees). You are not the counterparty to the bets and you do not risk principal on the outcome — that is what makes a VIZ market different from a "bookmaker" who can go into the red. + +## What you set at creation + +**Question and outcomes.** The text of the question, the metadata (title, event, tags, image link). The market type: **binary** (two outcomes, CPMM curve) or **multi-outcome** (up to 64 outcomes, LMSR). A multi-outcome market is convenient when there are many outcomes (which of N wins); details — in the article on multi-outcome markets. + +**Initial liquidity.** How much VIZ you put into the curve. The more, the "deeper" the market: bets move the price more smoothly, and large players can enter without sharp jumps. This is your principal-protected capital (see the article on the active LP). + +**Market fee.** The percentage withheld from bets and paid to liquidity providers (you and other LPs) as income. Capped from above by a network median parameter. + +**Oracle.** The account responsible for announcing the outcome. That can be you yourself or a trusted specialised oracle (for example, one mirroring Polymarket/Kalshi). The oracle carries an **insurance bond** and a reputation: it is penalised for an incorrect or missed resolution — see the article on the oracle. + +**Dispute mode and window.** How the outcome can be challenged after it is announced: by a committee or by an account vote, and within what grace period. This protects bettors from an incorrect resolution. + +**Deadlines.** `betting_expiration` — when bets stop being accepted; `result_expiration` — the deadline by which the oracle must announce the outcome. The `allow_early_resolution` option lets the oracle close the market earlier if the outcome is already known. + +## What happens after creation + +- **The market is open.** People bet, fees accrue to you as an LP. You can add more liquidity or withdraw part of it (price-neutral, with no loss of principal). +- **Betting closes.** At `betting_expiration` bets stop being accepted. +- **Resolution.** The oracle announces the outcome. The node splits the pool: those who called it right are paid automatically, and your LP principal + share of fees are returned at settlement **unconditionally**. +- **If there is no outcome.** The oracle declares **no-contest** (event cancelled, source disappeared) → bets are refunded to the bettors, your liquidity comes back to you, and the oracle is not penalised. But if the oracle **stayed silent** until the deadline (`result_expiration` + grace), the market dies as **missed-resolution**: bets are still refunded, but the oracle is penalised (see "[Oracle and resolution](./resolution)"). + +## What a creator needs to understand + +- **You are not the bookmaker.** Direct market liquidity does not cover the winnings out of your own pocket — the prize pool is formed by the losing bets. Your principal is protected. +- **The oracle is a critical choice.** Whether the market is resolved honestly depends on it. A bad oracle = disputes and penalties. Take a proven one, or be the oracle yourself and keep insurance above the risk-floor, otherwise your market will be hidden from the listings. +- **The fee is a balance.** A higher fee = more income for you, but a more expensive market for the players and less volume. A lower one is more attractive to bettors. +- **Depth decides.** Thin liquidity = sharp price jumps on every bet, which scares off large players. The initial stake sets the quality of the market. +- **Metadata matters.** A market with no on-chain title/tags is harder to find in clients and harder for automation to resolve. + +## Roles next to you + +- **Bettor** — the one who bets on your outcomes. +- **Oracle** — announces the result; you choose it at creation. +- **Active LP** — can add depth to your market on top of your initial stake. + +More on the topic: "Oracle and resolution" (who to choose and what they answer for), "Active LP" (the mechanics of liquidity and principal return), "Disputes" (dispute modes), "Multi-outcome markets" (when there are more than two outcomes). diff --git a/docs/prediction-markets/guides/multi-outcome.md b/docs/prediction-markets/guides/multi-outcome.md new file mode 100644 index 0000000000..ee769d84ae --- /dev/null +++ b/docs/prediction-markets/guides/multi-outcome.md @@ -0,0 +1,39 @@ +--- +title: "Multi-outcome markets — one question, many options" +description: "A multi-outcome market: one market for several outcomes (which of N wins), priced by LMSR instead of the binary CPMM. The outcome limit is the median parameter pm_max_outcomes; oversized markets are reduced to binary form. The bet, the shares and the payout work as in any pool." +--- + +# Multi-outcome markets: one question, many options + +Not every question boils down to "yes/no". "Who wins a tournament of eight teams?", "which party gets the most votes?" — here there are many outcomes. A multi-outcome market keeps them in **one** market instead of a pile of separate binary ones. + +## The gist in two paragraphs + +A binary market (two outcomes) is priced along a **CPMM** curve. When there are more outcomes, the market uses **LMSR** (logarithmic market scoring rule): one curve for all outcomes at once, where the price of each depends on how much has been staked on it relative to the others. The sum of the outcomes' "probabilities" stays consistent, and you always see the relative price of every option. + +For you as a participant the logic is the same as in any pool: you bet on an outcome, receive shares (weight) at the current price, and on resolution the prize pot is split among those who called it right in proportion to their shares. The difference is under the hood (the pricing formula) and in the limit: the number of outcomes is capped by the network's median parameter `pm_max_outcomes`; if there are more outcomes than the limit, the market is reduced to binary form (for example, "favourite vs the field"). + +## How it differs from a bundle of binary markets + +**One market instead of N.** Instead of ten separate "team X wins: yes/no" markets — one market with ten outcomes. Liquidity is not smeared across ten pools, and the prices are consistent across the options. + +**LMSR instead of CPMM.** A binary CPMM holds two sides; LMSR generalises that to many outcomes through a logarithmic cost function. The LMSR "depth" parameter (`lmsr_b`) is derived from the liquidity provided and the number of outcomes — it sets how expensive it is to move the price. The more liquidity, the deeper the market. + +**Consistent prices.** In LMSR the outcome prices are linked: one gets more expensive and the rest get relatively cheaper. That is closer to a "probability distribution" than a set of independent binary markets. + +**Relation to events.** Large events (a match, a tournament) are often mirrored from external sources as a set of markets under a common `event` key — then the multi-outcome market and the related binary props are grouped on the event page (see the Forecaster client). + +## What you need to understand + +- **The logic of a bet does not change.** Outcome → shares at the current price → a share of the pot on resolution. Multi-outcome differs in pricing, not in substance. +- **The outcome limit is a network parameter.** `pm_max_outcomes` is median-voted; a market with more outcomes than the limit is created in binary form rather than silently rejected. +- **Depth matters more with many outcomes.** Thin liquidity on a multi-outcome market moves prices more sharply — the LMSR depth is spread across all the options. +- **Entering early on an underpriced option pays off.** Just as in a binary pool: a cheap price means more shares. +- **The payout follows the shares.** No odds fixed in advance; the result comes out of the final distribution. + +## Related + +- [Why a pool and not odds](./why-pool-not-odds) — how a price is formed in a pool in the first place. +- [Bettor](./bettor) — the life cycle of a bet (the same for multi-outcome markets). +- [Market creator](./market-creator) — how to choose the market type at creation. +- [Specification](../specification) — the LMSR formulas, `lmsr_b`, outcome limits. diff --git a/docs/prediction-markets/guides/oracle.md b/docs/prediction-markets/guides/oracle.md new file mode 100644 index 0000000000..db82733f4b --- /dev/null +++ b/docs/prediction-markets/guides/oracle.md @@ -0,0 +1,47 @@ +--- +title: "Oracle — how to announce an outcome and answer for it" +description: "The oracle announces the market result (pm_resolve_market), carries an insurance bond and a reputation (reliability_score). Penalties for an incorrect/missed resolution; the risk-floor hides an underfunded oracle from the listings." +--- + +# Oracle: how to announce an outcome and answer for it + +An oracle is the account that tells the chain how the event ended. Who gets the prize pool depends on it. That is why an oracle on VIZ does not merely "press a button": it carries a **money bond** and a **reputation**, and answers with both for honesty and timeliness. + +## The gist in two paragraphs + +You announce the winning outcome with the `pm_resolve_market` operation (only after betting closes). Payouts to those who called it right are credited automatically. To be trusted with markets and money, you hold **insurance** — a VIZ bond tied to your oracle account. If insurance sags below the **risk-floor** relative to the volume of bets you serve, the node **hides your markets from the listings** — the mechanism protects bettors from an undercapitalised oracle. + +For mistakes and silence you are penalised out of your accrued reputation and bond: an incorrect resolution, challenged and overturned by a dispute; a missed deadline (`result_expiration`), when the market dies as missed-resolution. Your **reputation** (`reliability_score`, 0..10000 bp) is made up of accuracy, dispute verdicts, responsiveness and punctuality — and is visible to everyone choosing an oracle. + +## What you do step by step + +**Get ready.** Fund insurance on the oracle account (`pm_oracle_update`) — keep it with a margin above the risk-floor, otherwise your markets will be hidden. The floor rule: a market is visible if insurance ≥ the threshold and ≥ a multiple of the sum of bets under your management. + +**Wait for betting to close.** Resolution is only possible after `betting_expiration`. Not before (there is no point announcing an outcome while bets are still coming in). If the market was created with `allow_early_resolution` and the outcome is already known for sure, you can close it early — the dispute window then collapses to the moment of resolution. + +**Announce the outcome.** `pm_resolve_market` with the winning outcome. The node splits the pool: those who called it right are paid automatically (virtual `pm_payout`), and losing outcomes are zeroed out. For the resolution you are credited an **oracle fee** (capped by a network median parameter, at most `pm_max_oracle_fee_percent`). + +**If there is no outcome.** The event was cancelled, the source disappeared, a draw with no winner → instead of resolving you declare **no-contest** (`pm_no_contest`): bets are refunded, nobody wins and nobody loses. That is an honest exit, not a penalty — but it has to be done in time, before the deadline. + +**Get through the dispute window.** After the announcement, bettors can challenge the outcome within the grace period (`pm_dispute_create`). If the dispute finds you were right, your reputation is confirmed; if your resolution is found incorrect, you are penalised. Details — in the article on disputes. + +## What you are penalised for + +- **Incorrect resolution.** You announced the wrong outcome and a dispute confirmed it → penalty from the bond/reputation, payouts are recomputed. +- **Missed deadline.** You did not announce the outcome before `result_expiration` → the market dies as missed-resolution and you are slashed. Do not stay silent: if the source did not give a result, declare no-contest. +- **Systematic slowness.** Late (but completed) resolutions drag down the punctuality factor in `reliability_score`. Being late is no longer the same as being on time. + +## What an oracle needs to understand + +- **Insurance is trust in numbers.** Keep it above the floor with a margin; it sags from accumulated slashing and grows with the volume you serve. An underfunded oracle disappears from the listings — the markets seem to "vanish". +- **Reputation is public and composite.** `reliability_score` = accuracy + dispute verdicts + responsiveness + punctuality − penalties (decaying penalty-stamps) − bans, shrunk toward the average when the number of resolutions is small. In the interface it is shown as a percentage (bp/100). +- **A resolution is irreversible in effect, but disputable.** Payouts go out immediately, but the dispute window can cancel them and punish you. If you got it wrong, it is more honest to initiate the correction yourself than to wait for a slashing. +- **Timeliness = money.** A no-contest on time is better than silence until the deadline. Automate resolution if you serve many markets. + +## Roles next to you + +- **Market creator** — chooses you as the oracle and sets the dispute window. +- **Bettor** — trusts you with the outcome and can challenge it. +- **Disputer** — a participant in the dispute who checks your verdict. + +More on the topic: "Disputes" (how an outcome is challenged and what it means for the oracle), "Market creator" (who appoints you and how), "Oracle and resolution" (deadlines, missed-resolution, no-contest in detail). diff --git a/docs/prediction-markets/guides/passive-lp.md b/docs/prediction-markets/guides/passive-lp.md new file mode 100644 index 0000000000..684dfed9ac --- /dev/null +++ b/docs/prediction-markets/guides/passive-lp.md @@ -0,0 +1,40 @@ +--- +title: "Passive LP — the lazy pool" +description: "The lazy pool is a passive product: you deposit VIZ and earn from leverage markup and fees. Shares are minted at the equity price, withdrawals go through a FIFO queue (free_balance ≥ 0 invariant), and the penalty applies only to rewards. The pool is the counterparty to leverage traders." +--- + +# Passive LP: the lazy pool + +Don't want to pick a particular market and watch its depth — just put VIZ into the **lazy pool**. It is a passive product: your capital automatically feeds the system (leverage first of all) and earns yield, while you monitor nothing. + +## The gist in two paragraphs + +You deposit VIZ (`pm_lazy_deposit`) and receive pool **shares** at the pool's current equity price (not at face value: the share price is computed from `free + allocated − pending_withdrawals`, so that a new deposit made while capital is deployed does not get an inflated weight). The pool acts as the **counterparty to leverage traders**: a leverage loan is fronted by the lazy pool, and the markup plus the funding rate flow back into the pool as yield. On top of that comes a share of general fees. + +Your principal in the pool comes back, but the payout may enter a **FIFO queue** if there is not enough free balance right now (capital is deployed in open leverage positions). The pool's invariant is `free_balance ≥ 0`: the pool never pays out more than is actually free; a withdrawal request is registered and settled as funds return. The early-withdrawal penalty is taken **only from rewards** — the principal is not cut. + +## What happens, step by step + +**Deposit.** `pm_lazy_deposit` — VIZ goes into the pool and you receive shares at the equity price. From there the pool itself decides where to route the capital (leverage, depth); you do not steer this manually. + +**Yield accrues.** Leverage traders pay the markup and funding — that goes into the pool's yield. Your shares grow in value. The income is passive; there is nothing to click. + +**Withdrawal — planned or emergency.** `pm_lazy_withdraw` (partial, by shares, or everything). If the pool has enough free balance, the payout is instant. If capital is deployed, a request (`pm_lazy_withdraw_request`) enters the **FIFO queue** and is settled as funds return from leverage/depth. An emergency withdrawal takes a penalty — but **only from accrued rewards**; your principal is not reduced. + +**The queue and its order.** Requests are settled in arrival order on every event that returns free balance (leverage close, conversion, a new deposit). This protects the pool from going negative — a lesson from the early design, when an emergency withdrawal could drag `free_balance` below zero. + +## What a passive LP needs to understand + +- **The pool is the counterparty to leverage.** Unlike a direct market LP (curve depth, principal-protected against the outcome), the lazy pool carries the risk of leverage positions: its capital is borrowed by traders. The yield is higher, but the nature of the risk is different. +- **The share price is equity-based, not face value.** You receive shares at the pool's real value, not 1:1. That distributes yield fairly between old and new depositors. +- **A withdrawal may wait.** If all free balance is deployed, your request enters the queue. That is not a loss — the principal returns as positions unwind; but immediacy is not guaranteed. +- **The penalty hits rewards only.** An early/emergency withdrawal cuts yield, not principal. `free_balance ≥ 0` is a hard invariant. +- **Passivity is both a plus and a minus.** You do not pick markets and do not monitor depth, but you also do not control where the capital goes. + +## Roles next to yours + +- **Leverage trader** — borrows from your pool to bet on price; their markup is your income. +- **Active LP** — the opposite product in spirit: manual depth in a specific market, principal-protected against the outcome. +- **Market creator** — builds the markets on which leverage and depth operate. + +Further reading: "Leverage trader" (who borrows from the pool and how), "Active LP" (how it differs from direct liquidity), "The lazy pool in detail" (equity price, FIFO withdrawal, yield step by step). diff --git a/docs/prediction-markets/guides/resolution.md b/docs/prediction-markets/guides/resolution.md new file mode 100644 index 0000000000..41450411c7 --- /dev/null +++ b/docs/prediction-markets/guides/resolution.md @@ -0,0 +1,49 @@ +--- +title: "Oracle and resolution — how an outcome is announced" +description: "The market resolution mechanism: resolution only after betting closes, the result_expiration deadline, early resolution (allow_early_resolution), missed-resolution void after grace, no-contest when there is no outcome. Payouts are automatic." +--- + +# Oracle and resolution: how an outcome is announced + +This article is about the **mechanism** of resolution — the timeline and the rules common to all participants. For the duties and risks of the oracle itself there is a separate article, [Oracle](./oracle); here we look at how a market gets from betting close to payouts. + +## The gist in two paragraphs + +A market can only be resolved **after betting closes** (`betting_expiration`) — while bets are still coming in, announcing an outcome is pointless. A market has a `result_expiration` deadline by which the oracle must announce the result (`pm_resolve_market`). On resolution, payouts to those who called it right are credited **automatically** (virtual `pm_payout`), losing outcomes are zeroed out and their money goes into the winners' prize pot. + +If there is no outcome — the event was cancelled, the source disappeared, a draw with no winner — the market is closed as **no-contest** (`pm_no_contest`): bets are refunded, nobody wins and nobody loses. And if the oracle stayed silent past the deadline and the grace period, the market dies as **missed-resolution** (the oracle is penalised). There is also an early path: markets with `allow_early_resolution` can be closed sooner if the outcome is already known for sure. + +## Market timeline + +**1. Open.** Bets are coming in, the price floats along the curve. Resolution is forbidden. + +**2. Betting closes (`betting_expiration`).** Bets are no longer accepted. The window opens in which the oracle can (and must) announce the outcome. + +**3. Resolution (`pm_resolve_market`).** The oracle announces the winning outcome. The node splits the pool: payouts are automatic, there is nothing to claim by hand. For the resolution the oracle gets its fee (capped by a median parameter). + +**4. Dispute window.** After the announcement comes the grace period (`pm_dispute_grace_sec`) in which the outcome can be challenged (see [Disputes](./disputes)). Until the dispute is finalised the payouts are not final. + +**5. Settlement.** The confirmed outcome is settled: LP liquidity is returned (principal-protected) and the prize pot is distributed. + +## Special paths + +**Early resolution (`allow_early_resolution`).** If the market was created with this flag and the outcome is already known for certain, the oracle closes it early. `result_expiration` then shifts to the moment of resolution (the dispute window collapses to "now + grace"), but disputers still keep the full `pm_dispute_grace_sec` from the announcement. A late resolution (after `result_expiration`), on the contrary, does not extend the window. + +**No-contest (`pm_no_contest`).** There is no outcome — the market is cancelled and bets are refunded. This is not a penalty on participants: their money does not "burn" because a source went quiet. The oracle must declare no-contest in time if there is not going to be a result. + +**Missed-resolution (void on deadline).** The oracle did not announce an outcome and did not declare no-contest before `result_expiration` + grace → the cron voids the market and the oracle is slashed. Important: the void only fires **after** `result_expiration + pm_dispute_grace_sec` (the same cutoff as the settle-sweep) — so that the oracle has a real resolution window rather than a race against the deadline (this was fixed by the reachability fix; otherwise a fixed-deadline market without the early flag was impossible to resolve). + +## What to understand + +- **Resolution only after betting closes.** Not before; the early path is a separate market flag. +- **Payouts are automatic.** No "claim", no buttons: winnings and refunds arrive on resolution/cancellation. +- **Silence is punished.** No outcome → no-contest in time. Simply "doing nothing" = missed-resolution and a slashing of the oracle. +- **The outcome is not final until the grace period ends.** Within the dispute window payouts can be recomputed. +- **Leverage does not wait for resolution.** Leverage positions are closed at the price as of `betting_expiration`, independently of the oracle's verdict — see [Leverage trader](./leverage-trader). + +## Roles and links + +- [Oracle](./oracle) — who announces the outcome and what they answer with. +- [Disputes](./disputes) — how to challenge an announced result. +- [Bettor](./bettor) — what resolution looks like from the bet's side. +- [Specification](../specification) — the formal deadlines and cutoffs. diff --git a/docs/prediction-markets/guides/why-pool-not-odds.md b/docs/prediction-markets/guides/why-pool-not-odds.md new file mode 100644 index 0000000000..33bbc73aec --- /dev/null +++ b/docs/prediction-markets/guides/why-pool-not-odds.md @@ -0,0 +1,39 @@ +--- +title: "Why a pool and not odds" +description: "There are no fixed odds on VIZ: the price of an outcome is set by the pool curve (CPMM for binary markets, LMSR for multi-outcome ones), and the payout is a share of the prize pot proportional to the shares bought. The odds float until betting closes." +--- + +# Why a pool and not "odds" + +"What are the odds on this outcome?" is the most common question. On VIZ the honest answer is: **there are no fixed odds**, and that is a principle rather than an oversight. Let's go through how a pool differs from a bookmaker and how your winnings come out of it. + +## The gist in two paragraphs + +At a bookmaker the odds are named by **the shop** — it is the counterparty, it carries the risk and it earns on the margin. Someone has to be "the house". On a blockchain there is no "house": otherwise it would be an intermediary again, exactly the thing the ecosystem removes. So a VIZ market is a **shared pool**, and the price of an outcome is set by us, by our own bets. + +The price is neither pulled out of thin air nor locked in: it is set by the **pool curve** (CPMM for binary markets, LMSR for multi-outcome ones). Every bet moves the price: the more has been staked on an outcome, the more expensive it is and the fewer "shares" (weight) the next token buys. On resolution the losers' prize pot is split among those who called it right **in proportion to their shares**. Nothing is promised as "×2.5" in advance — the multiplier comes out of how everyone ended up positioned. + +## How it works + +**The price is the state of the curve right now.** When you bet, the node computes along the curve how many shares your amount is due at the current price. An early bet on an unpopular outcome means you entered cheaply and got a lot of shares. Once the crowd arrives, the price is different. + +**The simple "kitty" intuition.** Roughly: 100 Ƶ in total on "Yes", 300 Ƶ on "No", and "Yes" happens. The winners take back their own and split the losing 300 Ƶ by shares. A 10 Ƶ bet (a tenth of the "Yes" pool) → about 40 Ƶ on the way out. The exact number comes from the curve (a smooth price instead of the steps of a pure kitty), but the direction is the same: the fewer people on the winning side, the fatter the share. + +**The odds float until betting closes.** Since the payout is built out of live bets, it changes while the market is open. A large player enters on your side — your share of the pool shrinks and the result will be **lower** than it looked when you bet. That is not the interface lying to you, it is the nature of a pool. That is why the client shows an estimate, not "fixed odds". + +**Depth smooths things out.** Liquidity (direct from LPs plus the creator's starting stake) determines how sharply a bet moves the price. A deep market means a smooth price, a thin one means jumps. Liquidity works while the market is alive and comes back at settlement (principal-protected). + +## What you need to understand + +- **No house means no fixed odds.** The price is set by all the participants together; you see the current estimate, not a promise. +- **The payout is a share of the pot, not "bet × odds".** The result depends on the final distribution of bets across outcomes, not on the number you saw when you entered. +- **Entering early pays off more on an underpriced outcome.** More shares per token while the price is low. +- **Do not bet more than you are ready to lose.** Floating odds can move against you before betting even closes. +- **The parameters live on chain, not on a website.** Fees, penalties, limits — median voting by delegates, not the will of a shop. + +## Where to go next + +- [Bettor](./bettor) — how to place a bet step by step. +- [Active LP](./active-lp) — who sets the depth of the curve and why it is principal-protected. +- [Multi-outcome markets](./multi-outcome) — when there are more than two outcomes (LMSR). +- [Specification](../specification) — the formal formulas of the curve and of settlement. diff --git a/docs/prediction-markets/index.md b/docs/prediction-markets/index.md new file mode 100644 index 0000000000..26c322208d --- /dev/null +++ b/docs/prediction-markets/index.md @@ -0,0 +1,55 @@ +--- +title: Prediction Markets (Onix) — overview & map +description: The VIZ on-chain prediction-market stack — Onix protocol and the Forecaster client — with a full documentation tree from whitepaper to spec, objects, operations, workflows, and concept analysis. +--- + +# Prediction Markets on VIZ + +VIZ Ledger runs prediction markets as **first-class consensus operations** (`pm_*`), live since HF14. +Two names you will see throughout: + +- **Onix** — the **protocol**: the on-chain market engine (CPMM binary + LMSR multi, parimutuel + zero-sum settlement, bonded oracles, lazy pool, opt-in leverage, batch / commit-reveal betting). +- **Forecaster** — the **thin client** to that protocol on VIZ Ledger. It is a headless, platform- + independent front-end that lets **people anywhere in the world participate in the on-chain + prediction market** — create markets, bet, provide liquidity, oracle, and dispute — by signing + `pm_*` operations directly against public VIZ nodes. The protocol is neutral; Forecaster (and any + jurisdictional client built like it) is the access layer. + +## Documentation map + +```mermaid +flowchart TD + ROOT["Prediction Markets (Onix)"] + ROOT --> OV["Overview — the one-page pitch (this stack at a glance)"] + ROOT --> WP["Whitepaper — the thesis: why LP-risk-free, the two market types, the flywheel"] + ROOT --> SP["Specification — formal mechanics + §17 On-Chain Object Model"] + ROOT --> OPS["Operations — the signed pm_* consensus operations"] + ROOT --> VOPS["Virtual Operations — deterministic vops emitted at settlement / by deadline"] + ROOT --> API["Plugin API — prediction_market_api read methods"] + ROOT --> WF["Workflows & diagrams — one canonical market traced through every role"] + ROOT --> CA["Concept analysis — 90 PM-theory concepts vs the live VIZ implementation"] +``` + +## Start here + +| Page | What it is | +|------|-----------| +| [Guides (по ролям и фичам)](./guides/) | Пояснительные статьи для участников: беттер, создатель рынка, оракул, LP, плечевой трейдер — простым языком. | +| [Overview](./onix) | One-page positioning: AMM-priced parimutuel with structurally risk-free liquidity. | +| [Whitepaper](./whitepaper) | The industry thesis — LP guarantee, Onix Binary (CPMM) + Onix Multi (LMSR), oracles, lazy pool, leverage, governance. | +| [Specification](./specification) | Formal spec: parameters, state machine, pricing, settlement, disputes, lazy pool, leverage, and the **[On-Chain Object Model](./specification#17-on-chain-object-model)** (every `pm_*_object` and its lookup index). | +| [Operations](../protocol/operations/prediction-markets) | The 21 signed consensus operations (`pm_create_market`, `pm_place_bet`, …). | +| [Virtual Operations](../protocol/virtual-operations) | Deterministic virtual ops (`pm_payout`, `pm_market_accepted`, `pm_leverage_resolve`, `pm_batch_settle`, …). | +| [Plugin API](../plugins/prediction-market-api) | `prediction_market_api` — read-only access to markets, bets, oracles, disputes, the lazy pool, and the median-voted parameters. | +| [Workflows & interaction diagrams](./workflows) | One canonical binary market traced through every participant, with the zero-sum master ledger for normal and disputed resolution. | +| [Concept analysis (Onix vs 90 concepts)](./concepts-analysis) | How the on-chain implementation maps onto the prediction-market theory atlas — what's solved, inherent, not needed, or roadmap. | +| [Parlay & system bets — considered and rejected](./parlay-spec) | Why accumulators are **not** a VIZ primitive: a pool counterparty would break the principal-protection invariant LPs and the Lazy Pool rest on. Archived design kept as the argued record. | + +## Governance + +All economic parameters are delegate **median-voted** and live in the `chain_properties_pm` struct — +see [Chain Properties → Prediction-market parameters](../governance/chain-properties#pm-parameters). +There is no hard fork to tune fees, penalties, lazy-pool, leverage, or batch/commit-reveal timing; +three live kill-switches (`pm_commit_reveal_enabled`, `pm_lazy_pool_enabled`, `pm_leverage_enabled`) +let the validator median disable a whole subsystem without a fork. diff --git a/docs/prediction-markets/onix.md b/docs/prediction-markets/onix.md new file mode 100644 index 0000000000..93460b8af4 --- /dev/null +++ b/docs/prediction-markets/onix.md @@ -0,0 +1,93 @@ +--- +title: Onix — AMM-Priced Parimutuel Prediction Markets +description: Onix puts continuous AMM price discovery on top of parimutuel settlement, so prices move like an AMM while liquidity carries the risk profile of a tote — the market maker can never be bankrupted. +--- + +# Onix — AMM-Priced Parimutuel Prediction Markets + +> Continuous **AMM price discovery** on top of **parimutuel settlement** — the price moves like an +> AMM, while liquidity carries the risk profile of a tote: **the market maker can never be +> bankrupted.** + +::: info Onix & Forecaster +**Onix** is the on-chain protocol. **Forecaster** is the thin client to it on VIZ Ledger — the headless, +platform-independent access layer that lets people anywhere in the world participate in the on-chain +prediction market by signing `pm_*` operations directly against public VIZ nodes. See the +[section overview & map](./) for the full documentation tree. +::: + +## The one idea + +Onix **decouples price from payout**: + +- **Price (discovery)** — a CPMM curve (binary) or LMSR-softmax (multi) updates a live probability on + every bet and assigns each bet a **weight** (its claim ticket). +- **Payout (settlement)** — winners are paid **only** from the losers' forfeited stakes, split by + weight: pure **parimutuel**, strictly zero-sum (the protocol never mints a token). + +Everything distinctive about Onix follows from this split. + +## Why it matters — three things + +::: tip 1 · Liquidity that cannot be drained +Because winners are paid from losers and never from LP principal, the liquidity provider **cannot be +bankrupted** — no impermanent loss, no inventory risk, no death-by-sniper. Guaranteed *by construction* +(AM–GM for CPMM, conservation for LMSR), not by insurance. +::: + +::: tip 2 · Passive yield without IL — the Lazy Pool +One deposit auto-spreads as silent liquidity across many markets and funds opt-in leverage, with +MasterChef-style reward accounting. Earn prediction-market liquidity yield **without** picking markets +or bearing impermanent loss. +::: + +::: tip 3 · Native to the chain, zero-sum +Markets are first-class consensus operations (`pm_*`), not smart contracts: censorship-resistant, +composable, ~3-second blocks, no oracle bridge. The protocol never emits tokens — it only redistributes. +::: + +## How a bet works + +1. **You bet** `X` on an outcome. `X` enters the curve; the curve returns your **weight** — more weight + if you bet earlier, before the price moves. +2. **The board updates.** The live coefficient for a side is + `1 + opposing_pool × (1 − commission) / own_pool`, with the commission (oracle + creator + LP) already + baked in. +3. **At resolution**, the losers' stakes (minus commission) are split among the winners by weight. Your + payout = your stake back **+** your share of the losing pool. LP principal is returned untouched. + +## How it compares + +| | CLOB / AMM (Polymarket, Kalshi) | Plain parimutuel (tote) | **Onix** | +|---|---|---|---| +| Live price | yes | no (pool ratio only) | **yes (CPMM / LMSR)** | +| Odds locked at bet time | yes | no | no (honest parimutuel) | +| LP / maker can be bankrupted | **yes** (IL, snipers, gap risk) | n/a | **no (structural)** | +| Yield-bearing liquidity layer | fragile | none | **Lazy Pool, no IL** | +| Lives in | contracts / backend | backend | **consensus (`pm_*`)** | +| Token emission | sometimes | no | **no (zero-sum)** | + +## The honest tradeoff + +::: warning Odds are parimutuel — they drift until close +Onix does **not** lock your coefficient at bet time. The board moves as money flows, and the final +coefficient is known only at close — exactly like a tote. This is not a flaw to patch: the *only* way to +lock odds is to have a counterparty bear the risk (a bookmaker, or an AMM LP that can lose). Onix's drift +is the direct price of its LP guarantee — risk lives **between bettors**, so no one's liquidity can burn. +::: + +## What's novel + +- **AMM weighting + parimutuel settlement** in one integrated engine — continuous price discovery + *without* maker inventory risk. +- **Structural, provable LP safety** instead of insured or subsidized liquidity. +- **A mutualized, yield-bearing liquidity layer** (the Lazy Pool) that also funds opt-in leverage — + liquidations run against pre-bet reserves so the pool is always made whole. +- **Opt-in anti-MEV** (batch / commit-reveal betting) and **transparent governance** (bonded oracles, + public-hearing disputes with revisable votes) — all layered on the safe base without ever touching the + LP guarantee. + +## Learn more + +- Protocol operations — [Prediction Markets](../protocol/operations/prediction-markets) +- Plugin API — [Prediction Market API](../plugins/prediction-market-api) diff --git a/docs/prediction-markets/parlay-spec.md b/docs/prediction-markets/parlay-spec.md new file mode 100644 index 0000000000..1567dfe635 --- /dev/null +++ b/docs/prediction-markets/parlay-spec.md @@ -0,0 +1,261 @@ +# Parlay (accumulator) & system bets — considered and REJECTED + +Status: **rejected as a consensus primitive** (owner decision 2026-08-19, q#603=A). The full +design below is kept as an archived record of *why* the idea does not fit the protocol, so the +next "let's add parlays" proposal starts from the argued rejection, not from scratch. + +## Why rejected + +The platform's core trust invariant is that **liquidity providers and the Lazy Pool are +principal-protected by construction**: betting is zero-sum between bettors (winners split the +losers' pool), and the pool/LPs only collect fees and floors. A depositor does not have to trust +market creators or oracles with their principal. That invariant is what makes a *decentralized* +prediction market with permissionless market creation and competing oracles viable at all. + +A real parlay needs a counterparty holding directional risk at odds fixed at bet time. Making the +Lazy Pool that counterparty breaks the invariant — depositors become hostages of every market +creator's quality — and it is not fixable with parameters: + +1. **Leg correlation is a structural adverse-selection hole.** `W = S·(1−m)/Π p_i` is only fair + for *independent* legs. In a permissionless world, market creators can construct correlated + legs at will ("X wins the match" + "X wins map 2", the same real-world fact wrapped by two + different oracles). `Π p_i` systematically underprices such combos, giving the attacker a + persistent +EV against the pool. Correlation between markets is real-world semantics — + undetectable on-chain in principle. Centralized bookmakers solve this with human traders and + per-combo limits; the protocol has no such layer. +2. **Leg prices come from manipulable curves.** Execution-price quoting (q#600=A) defends against + one-shot curve manipulation right before opening, but a thin parimutuel curve is still not an + honest probability. Fixed odds against a price source the attacker can influence means the + pool pays for someone else's control of the source. +3. **What it took to make the one existing pool-fronted product safe (leverage, F1/#300).** + Leverage is the only place where the pool fronts funds, and it produced exactly this failure + class: position profit exceeding the losers' pool, shortfall landing on LPs. It is **solved** — + the early-exit reward cap plus the deferred outcome-contingent claim make + `winners_pool ≥ (1−cap)·losers − fees ≥ 0`, i.e. `uncovered == 0` **by construction**, with the + LP-charge path kept only as a defensive fallback behind a loud invariant-violation log. The + point is what that guarantee costs: leverage is a *bounded, collateralized* loan with + liquidation sweeps, and it still took a dedicated cap, a deferred-claim design and an + always-on invariant to bound. A parlay book has multiplicative payouts, no collateral to + liquidate and no per-leg bound to cap against — the same guarantee has nothing to hang on. +4. **Consensus complexity vs. one UX feature.** Ten new median parameters, new objects, new + settlement paths (void re-pricing, dispute interactions, escrow FIFO) — all of it money-path + attack surface to audit before mainnet. + +A parlay book works when the market maker is a centralized, fully trusted party. That is +explicitly not this protocol's trust model. + +## What replaces it + +- The **coupon** (one transaction, N independent `pm_place_bet` ops) already ships in the + Forecaster client — a multi-bet without a counterparty. +- A client-side **auto-roll** ("sequential parlay") can deliver the accumulator feel with zero + consensus changes: the client re-stakes a leg's winnings on the next leg after it resolves. + Counterparty = the ordinary parimutuel pools; odds are not fixed upfront, which is honest under + parimutuel pricing. May later be hardened with a small "conditional bet after market X + resolves" operation if on-chain execution guarantees are wanted. +- If a fully trusted market maker ever emerges, a parlay book could run as a **separate opt-in + risk fund** (explicitly *not* the Lazy Pool), where depositors knowingly accept bookmaker risk. + Out of scope until then. + +--- + +# Archived design (pre-rejection, 2026-08-18) + +Everything below this line documents the design as it stood before the rejection, including the +q#600/q#601 scope decisions that were locked while it was still a candidate. It is retained for +reference only — none of it is planned work. + +## Problem + +The coupon shipped in the Forecaster client (one transaction carrying N independent +`pm_place_bet` operations) is a *multi-bet*, not a parlay: each leg settles on its own, wins and +losses are independent. A **real parlay (accumulator/экспресс)** is a single stake on the +*conjunction* of N outcomes: it pays only if **every** leg wins, and the potential payout +multiplies the legs' odds. A **system bet "M of N"** is the standard generalization: the stake is +split across all C(N,M) M-leg sub-parlays, so the ticket survives up to N−M losing legs. + +Parimutuel markets have no fixed odds — a leg's final coefficient is known only when its pool +closes. So a naive "multiply final parimutuel coefficients" parlay cannot be funded by the legs' +own pools: a cross-market conjunction payout is not backed by any single market's losers. The +parlay needs an explicit counterparty and a price fixed at bet time. + +## Design summary + +- **Counterparty: the Lazy Pool** — the same inventory-bearing fund that already fronts leverage + loans. A parlay is a side bet against the pool at curve prices; it does **not** touch the legs' + curves or pools. +- **Price fixed at bet time** from each leg's live curve (CPMM for binary, LMSR-softmax for + multi): combo price `P = Π p_i`, potential payout `W = S · (1 − pm_parlay_margin) / P`, capped. +- **All-or-nothing settlement** driven by the legs' regular oracle resolutions: any leg lost → + ticket dead immediately; a voided (no-contest) leg is *excluded* (its `p_i` multiplied back + in — the bookmaker standard); all remaining legs won → pool pays `W` automatically after the + last leg settles. No claim operation, consistent with `pm_payout` auto-payout philosophy. +- **Worst-case escrow**: the pool locks `W − S` at open, so every open ticket is fully funded by + construction; the stake `S` enters `pool.free_balance` immediately. + +## Mechanics + +### Opening: `pm_parlay_open` + +``` +pm_parlay_open { + account, + legs: [ { market_id, side (binary) | outcome_index (multi) }, ... ], + amount, // stake S, liquid VIZ + min_payout, // slippage guard on W (curve may move between quote and inclusion) + extensions +} +``` + +Validation / evaluator gates (all loud `FC_ASSERT`s): + +1. `2 ≤ legs.size() ≤ pm_parlay_max_legs`; all `market_id` distinct. +2. Every leg market: status 1 (active), betting still open **with at least + `pm_parlay_min_time_left` seconds** to that leg's `betting_expiration` (anti-sniping: parlays + are priced on the live curve, so late steam on a nearly-closed leg is the cheapest attack). +3. Every leg market allows instant bets (`allow_instant_bet`), is **not** hidden below the + oracle risk-floor, and its curve depth passes the manipulation gate (below). +4. `pm_parlay_enabled` median kill-switch is on; the pool has capacity (below). +5. `S ≥ pm_min_bet`; account has liquid `S` (same funding rules as `pm_place_bet`). + +**Leg price `p_i`** is the **execution price of the leg's proportional virtual size**, not the +mid: quote the curve for a hypothetical instant bet of `S` on that side/outcome and use the +resulting average price. Mid-price quoting hands an attacker the spread for free; execution +pricing makes moving a thin curve *before* opening a parlay pay the mover's own slippage first. +The virtual quote does **not** mutate the curve. + +**Combo payout**: + +``` +P = Π p_i (0 < p_i < 1, so 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) // user slippage guard +FC_ASSERT(W > S) // a parlay that cannot profit is a mis-click, reject +``` + +**Funding at open** (single balanced move, conservation-exact): + +``` +account.balance -= S +pool.free_balance += S +pool.parlay_fund_used += (W − S) // worst-case escrow, W − S > 0 by the assert above +pool.free_balance -= (W − S) +``` + +Capacity gate: `parlay_fund_used + (W − S) ≤ free-only base × pm_parlay_fund_percent` — the same +free-only base rule the owner fixed for leverage (q#566=A): obligations are measured against +`free_balance` only, never NAV. + +### Object + +``` +pm_parlay_object { + id, account, + legs: [ { market_id, side, outcome_index, price_ppm, // p_i fixed at open, 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 +} +``` + +Indexes: `by_id`, `by_account`, and **`by_market_leg` (market_id → parlay ids)** so per-market +resolution can find affected tickets without scanning. The per-market fan-out is bounded by +`pm_parlay_max_open_per_market` (cap enforced at open via bounded index probe — counter-free, +see the commit-cap precedent M4 and the computed-vs-counter rule). + +### Settlement + +Hooked into the same per-block `process_pm_markets()` walk that already finalizes payouts — +parlay legs react to the leg market reaching **settled** state (post dispute-grace), not to the +raw resolve, so dispute reversals are automatically respected: + +- **Leg lost** → ticket `status = 2` immediately: release the escrow + (`parlay_fund_used -= (W − S)`, `free_balance += (W − S)`). The stake already sits in the pool + — it *is* the pool's revenue on lost tickets. Emit `pm_parlay_lost` virtual op. +- **Leg void** (no-contest / missed-resolution void) → `state = 3`; payout shrinks: + `W' = W · p_i` (multiply the excluded leg's price back in), clamped `W' = max(W', S)`; release + the escrow delta. If **all** legs void → refund `S` (`status = 3`, pool pays back the stake, + full escrow released). Emit `pm_parlay_leg_void`. +- **Leg won** → `state = 1`; when the **last** pending leg settles won: pay + `pool.free_balance -= W; account.balance += W;` release escrow bookkeeping + (`parlay_fund_used -= (W − S)`; the extra `W − S` was already carved out of free at open, so + paying `W` nets free_balance `−S` versus pre-open — exactly the pool's loss on a won ticket). + `status = 1`, emit `pm_parlay_won` (per-account virtual op for account_history). + +Work per settled market is bounded: at most `pm_parlay_max_open_per_market` tickets touched, each +O(legs) ≤ `pm_parlay_max_legs`. No unbounded per-block loops (audit class H3/M3). + +**Invariants** (debug-asserted, snapshot-import verified like the TOKEN anchor): + +1. `parlay_fund_used == Σ_open (W_i − S_i)` — recomputable by walking open tickets. +2. `pool.free_balance ≥ 0` always (FIFO-queue rule untouched; parlay payouts go through the same + "never below zero" discipline — escrow guarantees the funds exist). +3. Ticket terminal states are absorbing; `last_settled_leg_count` monotonic. + +### System bets "M of N" + +One operation, `pm_system_open`, same leg rules plus `2 ≤ M < N ≤ pm_parlay_max_legs` and +`C(N,M) ≤ pm_system_max_combos` (e.g. 256 — keeps worst-case settlement work and escrow math +trivially bounded). Semantics: stake `S` splits into `C(N,M)` equal sub-stakes, each sub-parlay +priced/capped exactly as above from the same fixed `price_ppm` set; escrow = Σ over combos. +Stored as one object (legs + M + per-combo derived data computed on settlement, not stored). +"7 из 8" = M=7, N=8, 8 combos. Settlement: on last leg settle, count won/void legs, enumerate +combos arithmetically (no recursion), pay Σ of winning combos' payouts. Refund/void/shrink rules +apply per combo. Deferred to **phase 2 of implementation** but specified now so the object layout +and params don't churn (snapshot-layout lesson: batch B → redeploy-only-by-snapshot). + +## Adversarial review (pre-implementation) + +| Attack / failure class | Vector here | Mitigation in this design | +|---|---|---| +| Curve manipulation (the main one) | Push a thin leg's curve, buy the parlay at distorted `p_i`, unwind | Execution-price quoting (mover pays own slippage), `pm_parlay_min_depth` gate per leg (min curve liquidity), `pm_parlay_margin` house edge, hard caps `max_payout`/`max_multiplier`, `min_time_left` window | +| Unbounded accumulation (#141 class) | `parlay_fund_used` grows, later subtracted | Escrow released on every terminal transition, recomputable invariant 1, clamp at 0 with loud ilog on mismatch | +| Sign-flip / underflow | `W − S`, `W' = W·p_i` shrink, refunds | `W > S` asserted at open; void-shrink clamped at `S`; all subtractions clamped `max(x,0)` + debug-assert | +| Missing floor/assert | "escrow covers payout by construction" | Explicit debug-assert on invariant 1 each maintenance block + snapshot-import re-check (anchor pattern) | +| DoS / per-block work | Many tickets on one market; many legs | `max_open_per_market` (bounded index probe), `max_legs`, `max_combos`, settlement O(tickets×legs) bounded | +| Governance extremes (F3 class) | Median sets margin=0 / multiplier=10^9 | `validate()` bounds on **every** new parameter (margin ≤ 20%, multiplier ≤ 10000×, legs ≤ 16, combos ≤ 1024, percent params bp-checked ≤10000) — and every param **wired into the median loop** (retention-param lesson) | +| Oracle/dispute interplay | Pay before dispute settles, then reversal | Legs react to *settled* (post-grace) state only, same cutoff as `pm_payout` sweep | +| Self-dealing LP | Bettor is also pool depositor | No special path needed: pool P&L is socialized exactly like leverage; margin + caps bound extraction | +| Snapshot round-trip | New object/fields lost on import | Full-reflect export; import with `contains()` guards; forward-only counters get seeds or are recomputable (invariant 1 is recomputable — preferred) | + +## New governance parameters (chain_properties, next version bump) + +`pm_parlay_enabled` (kill-switch, default **off** — leverage precedent), +`pm_parlay_margin` (bp, default 500 = 5%, bound ≤ 2000), +`pm_parlay_max_legs` (default 8, bound 2..16), +`pm_parlay_max_multiplier` (default 1000×, bound ≤ 10000), +`pm_parlay_max_payout` (VIZ, default 100k), +`pm_parlay_fund_percent` (bp of pool free, default 2000, bound ≤ 5000), +`pm_parlay_min_depth` (VIZ, default 1000), +`pm_parlay_min_time_left` (sec, default 3600), +`pm_parlay_max_open_per_market` (default 1000, bound ≤ 10000), +`pm_system_max_combos` (default 256, bound ≤ 1024). + +All ten must appear in: `validate()` with bounds, the median-vote loop, `get_pm_chain_properties`, +serializers (C++ ⇄ js ⇄ php ⇄ python lock-step — vop/param drift lesson from P1), and the +snapshot export/import of `chain_properties_pm`. + +## Client surface (after node lands) + +Coupon screen grows a mode switch: **Multi** (today's N independent bets) / **Экспресс** (one +`pm_parlay_open`) / **Система M из N** (phase 2). The coupon already collects legs in exactly the +right shape; the parlay quote (`Π p_i`, potential payout, caps) is computable client-side from +the same curve reads the bet form uses, with `min_payout` as the slippage guard. Read API: +`get_account_parlays`, `get_market_parlays` (newest-first default per q#383=A), parlay card in +activity (History/Active tabs). + +## Decisions log + +- **q#600=A (2026-08-18):** leg price = execution price of the stake's virtual size on the live + curve (not mid) — the curve manipulator pays their own slippage first. +- **q#601=A (2026-08-18):** first round = binary legs + plain parlay; M-of-N systems and multi + (LMSR) legs are phase 2. Object layout for systems is still specified above so the state shape + doesn't churn between rounds. +- Launch defaults for the new `pm_parlay_*` parameters (margin 500 bp, max_payout 100k VIZ, + max_multiplier 1000×, max_legs 8, kill-switch default **off**) stand as proposed unless the + owner overrides specific values before implementation; all are median-votable post-launch + anyway, the defaults only seed the very first median. diff --git a/docs/prediction-markets/settlement-work-bounds.md b/docs/prediction-markets/settlement-work-bounds.md new file mode 100644 index 0000000000..a9316131d6 --- /dev/null +++ b/docs/prediction-markets/settlement-work-bounds.md @@ -0,0 +1,553 @@ +# Bounding settlement work per block (#432) + +Status: fix **A** implemented (default confirmed at 1.000 VIZ) and fix **D** complete — garbage +collection, settlement and the void refunds all run on a metered row budget. This note records the +problem, the options weighed, and why the chain takes both. + +Sibling internal specs: [early-exit-deferred-claim](./early-exit-deferred-claim.md), +[specification](./specification.md) §5 (crons). + +## 1. The hole + +`pm_processing_cap_per_block` (median-voted, default 200) is the only limiter on the PM cron. +It counts **markets**, not work: + +``` +while (it != idx.end() && ... && done < cap) { // §5 auto-payouts + settle_market(*this, mkt); // touches EVERY bet row of the market + ++done; // ...and costs exactly ONE unit of cap +} +``` + +`settle_market()` (`libraries/chain/pm_process_markets.cpp`) walks the whole `by_market` bet +range, builds winner/loser vectors, pays each row through `adjust_balance`, flips its status and +pushes one virtual operation per row. There is no cursor and no resume: a market settles +entirely inside one block or not at all. The same shape appears in `gc_market()` (drops the +whole object cluster of a market in one block) and in the void/no-contest branch. + +Nothing bounded the number of rows a market can carry: + +* every `pm_place_bet` creates a **new** `pm_bet_object` — there is no aggregation by + (account, market, outcome); +* the instant path had **no minimum bet at all** (`amount > 0` and `tokens_out > 0` were the only + gates), so a row cost 1 raw = **0.001 VIZ**. Verified by live broadcast on the testnet, with + both a reject-control and an accept-control, at 0.001 / 0.002 / 0.010 VIZ; +* a **partial** `pm_transfer_position` splits one row into two at no stake cost whatsoever — + cheaper than betting, and it bypasses any bet-side floor; +* the same class of cap already existed everywhere else — `MAX_PM_DEFERRED_CLAIMS_PER_MARKET`, + `MAX_PM_DISPUTE_VOTES_PER_MARKET`, `MAX_PM_OPEN_COMMITS_PER_MARKET`, all 10 000. Bet rows were + the one member of the class left open. + +This does not need an attacker. A merely **popular** market walks into it: on the testnet, a toy +bot betting from three accounts every ten minutes had already accumulated 734 rows on one market +(571 and 552 on two others). A mainnet market with thousands of participants is orders of +magnitude larger, and all of that work lands in the single block where the dispute grace expires. + +## 2. Options + +| | Fix | Bounds work? | Cost | +|---|---|---|---| +| A | minimum bet on the instant path (mirror of `pm_min_batch_bet`) | no — only prices rows | one assert, median-tunable | +| B | hard cap on rows per market | yes | a popular market stops accepting bets: censorship / broken UX | +| C | aggregate bets by (account, market, outcome) | bounded by accounts | invasive: breaks per-bet `weight`, `time_penalty`, `entry_liquidity`, transfer_position, F1 claims | +| D | incremental settlement: bounded rows per block, cursor on the market | yes | largest consensus diff | + +**Chosen: A + D.** + +* **A alone is not a fix.** It raises the price of a row by three orders of magnitude + (0.001 → 1.000 VIZ) and it is votable, but the bound it gives is economic, not structural: + 1 000 000 rows at 1 VIZ is 1 000 000 VIZ, which is a lot of money but not an impossible amount — + and it is *staked*, not spent, so a large share comes back at payout. More importantly, A does + nothing at all about the legitimate case: a genuinely popular market is not spam and must not be + punished, yet it is the same block-time problem. +* **D alone is not enough either.** It makes the work per block finite, but leaves rows free, so a + spammer can still stretch one market's settlement over thousands of blocks and force every node + to carry the state. A is the cheap economic guard that keeps D's queue short. +* **B is rejected**: refusing bets on a market that is doing well is a user-visible failure of the + product, and the cap value would have to be guessed. +* **C is rejected**: it is a wider and riskier diff than D for the same benefit, and it destroys + per-bet properties that the settlement math and the early-exit claims depend on. + +## 3. Fix A — minimum bet (implemented) + +Two median-voted parameters were added to `chain_properties_pm`: + +* `pm_min_bet` — default `1.000 VIZ`, governance floor `0.1 VIZ` (`validate()`), mirror of + `pm_min_batch_bet`; +* `pm_settle_rows_per_block` — default `2000`, range `[100, 100000]`, consumed by fix D. + +Both are wired into the median loop in `database.cpp`. A PM parameter that is declared, reflected +and validated but never enters that loop is silently un-votable and frozen at the code default — +that already happened once with `pm_closed_market_retention_sec`. + +Enforcement points (`libraries/chain/pm_evaluator.cpp`): + +1. `pm_place_bet`, `mode == 0` (instant) → `amount >= pm_min_bet`; +2. `pm_place_bet`, `mode == 1` (queued batch) → `amount >= pm_min_batch_bet`. This path creates + rows too and had no floor either — only the *commit* path was covered; +3. `pm_transfer_position`, partial → **both** the transferred part and the remainder must stay at + or above `pm_min_bet`. A position below the floor is not trapped: it can still be transferred + whole, which moves the row instead of splitting it. + +4. `pm_add_liquidity` → `pm_min_liquidity`. Liquidity is the fourth row source and was the one that + got away initially: every call mints its own `pm_liquidity_object` (contributions are not merged + per provider) and the evaluator asserted only `amount > 0`, so rows could be minted at 1 raw + apiece while the bet paths were floored. The floor is the same one that already gates creating a + market, so the minimum ticket for putting up liquidity does not depend on whether you open the + market or top it up later — a product decision, taken deliberately rather than by default. + +## 4. Fix D — incremental settlement (design) + +The market carries its own settlement cursor and the cron spends a **global** per-block row +budget (`pm_settle_rows_per_block`, shared across all settling markets, oldest market first). +`settle_market` becomes a phase machine resumed block after block: + +| phase | work per row | budgeted | +|---|---|---| +| 1 force-close | close leveraged positions still open at settlement | no — see below | +| 2 aggregate | refund queued rows (status 5/6), sum `losers_sum` and winner weight | yes | +| 3 claims | pay outcome-contingent early-exit claims from the bounded bucket | yes | +| 4 payout | pay winners / flip losers, one virtual op per row | yes | +| 5 finalize | fees, LP settlement, dust, `payout_status = 3`, `finalized_time` | no — see below | + +Only the bet walk is metered, because only a bet row is cheap. Phases 1 and 5 iterate leveraged +positions and liquidity rows, and each of *those* costs `pm_min_liquidity` (100 VIZ) to create — +a hundred times the price of a bet row. Their work is bounded economically, by what an attacker +would have to stake to create the rows, so metering them would add cursors and resume state for a +threat that fix A already prices out. If that ever changes (a cheaper way to mint an LP or leverage +row), those phases need the same treatment and the same `escrow` discipline. + +#### Where the resume state lives + +Walking the current `settle_market()` end to end gives the exact state a paused settlement has to +carry, and it is more than a cursor: the money split is a two-pass algorithm. Pass one produces the +aggregates (`losers_sum`, total winner weight), pass two turns them into per-row payouts. Cut the +function at any block boundary and both passes need their partial results preserved, plus the +running totals that finalization needs for the dust. + +That state does **not** go on `pm_market_object`. It is twelve fields carried by every market that +ever existed, when only the handful currently settling can use them — the object is already ~40 +fields wide and markets are the most numerous object on the chain. Instead one +`pm_settlement_object` is created when a market enters settlement, keyed uniquely by market, and +removed at finalization, so the cost is proportional to settlements *in flight*: + +| field | phase | meaning | +|---|---|---| +| `phase`, `cursor` | all | current phase and the next row id to process in it | +| `stake_total` | 2 | `losers_sum` (normal) / total active stake (void) | +| `weight_total` | 2 | Σ winner curve weight, 128-bit as in `compute_settlement` | +| `winners_pool`, `uncovered` | set at 3→4 | the split constants, once claims are final | +| `distributed` | 4 | Σ profit paid — finalize routes `winners_pool − distributed` as dust | +| `lp_bonus` | 4 | Σ time-penalty taken from winners | +| `paid_claims` | 3 | drawn from the bounded early-exit bucket | +| `escrow` | all | signed conservation accumulator (below) | + +Per-winner payout depends only on `winners_pool`, `weight_total` and the row's own fields, so phase +4 needs no memory of the rows it already paid — that is what makes the cut clean. The void path +reuses the same fields (its two pro-rata distributions accumulate in `distributed` and `lp_bonus`), +keeping "the last participant absorbs the remainder" rounding identical to today's. + +Because the object is removed at finalization, a market that is *not* settling has no settlement +row at all, and garbage collection drops it with the rest of the cluster. + +Rules that make it safe: + +* **Determinism.** Phase, cursor and accumulators live in the settlement object; the budget is a + median-voted parameter. Every node therefore processes exactly the same rows in the same blocks. +* **The market is closed to everything else while settling.** `payout_status = 4` ("settling") + keeps §5 from re-entering, keeps `pm_dispute_create` out (it requires `payout_status == 1`), and + GC cannot fire because `finalized_time` is stamped only in phase 5. +* **Conservation at every block boundary.** Money released from a row but not yet paid out is held + in an explicit `escrow` accumulator: `+= amount` when a row is released, `-= payout` when someone + is paid. The PM supply invariant counts it as PM-held, so a snapshot taken mid-settlement + balances exactly; finalize asserts it reaches zero. The accumulator is **signed**: phase 3 pays + early-exit claims out of a losing pot whose rows are still standing, so it legitimately goes + negative before phase 4 releases them. That is not a deficit — the tokens are in real account + balances and the rows that will fund them are still counted as PM-held, so the two sides of the + invariant move together either way. +* **Progress.** A market with N rows finishes in about N / budget blocks; the floor of 100 on the + budget makes starvation impossible. +* **The rows themselves cannot move.** Every operation that creates, splits or removes a bet row — + `pm_place_bet`, `pm_commit_bet`, `pm_reveal_bet`, `pm_cancel_bet`, `pm_transfer_position` — + asserts `mkt.status == 1`, and a settling market is at status 3. So the set walked in phase 2 is + exactly the set paid in phase 4, with no gate to add. + +One cross-section interaction does **not** hold automatically and the implementation has to close +it: cron §1 (forfeit of commitments never revealed) adds the penalty to `forfeit_pool` of +*whatever* market the commitment belongs to, without looking at its status. Today that is harmless +— §1 runs earlier in the same block than §5, so settlement reads a final `forfeit_pool` — but a +settlement spanning blocks can have `forfeit_pool` grow *after* phase 3 has already folded it into +`winners_pool`, and those tokens would then belong to no one (orphaned until the GC burn, i.e. the +drift-400 failure mode again). Timing makes it unlikely in practice — a reveal deadline sits at +betting close, long before `result_expiration + grace` — but "unlikely in practice" is precisely +the reasoning that produced the earlier drifts. Phase 5 must therefore route any `forfeit_pool` +that appeared mid-flight instead of assuming it is zero, and the finalize-time escrow assert has to +account for it. + +### 4.1 Shipped: bounded garbage collection + +Collection went first — it is the same unbounded walk with none of the settlement arithmetic, so it +validates the budget plumbing on its own. `gc_market()` became `gc_market_step(db, mkt, budget)`: +it drops at most `budget` objects, decrements it in place, and returns true only when the whole +cluster (market object included) is gone. A market too large for one block keeps its place at the +head of the sweep — `finalized_time` never changes — and continues next block. + +No cursor is needed, unlike settlement: every range is re-entered at its `lower_bound` and the rows +already removed are *gone*, so the sweep resumes exactly where it stopped. Two details make the +pause safe: + +* the `forfeit_pool` burn is now zeroed in the same step, otherwise re-entry would burn the same + tokens again on every block and push `current_supply` below the accounted sum; +* a half-collected market is inert — terminal (`status 3` / `payout_status 3`), so no operation can + reach it, and the rows being dropped hold no money (bets `2/3`, LP `3`, leverage terminal), so the + supply invariant is flat across the pause. + +Covered by `gc_row_budget_spans_blocks` (consensus_sim): a 143-row cluster with the budget at its +floor of 100 must take more than one block and must never lose more than 100 rows in any block. +Verified against a deliberately unbounded control — with the budget bypassed the same test reports +"a single block removed 143 rows" and "collected in 1 block", i.e. the pre-fix behaviour. + +Settlement is served before collection in the block, so a heavy settlement backlog can defer GC. +That is harmless: it only stretches retention, and settlement is finite. + +### 4.2 Shipped: incremental settlement + +`settle_market()` became `settle_market_step(db, mkt, budget)`, driven by the `pm_settlement_object` +described above and spending the same global row budget as collection. Three things the +implementation had to get right, none of them visible from the design sketch: + +* **"Am I the last row?" cannot be answered by looking ahead.** Today's code hands the rounding + remainder to the last participant, which it recognises by peeking at the rest of the market. Under + resume that peek is both wrong (the rows already paid are still in the range, just terminal) and + quadratic. Phase 2 therefore counts the rows it aggregates into `rows_total` and phase 4 counts + what it has paid into `rows_done`; the last row is `rows_done == rows_total`, in O(1) and stable + across a pause. +* **The void branch has to release what it burns.** When a voided market has no participants left to + absorb the leftover pots, the leftover is burned — and burning it without subtracting it from + `escrow` trips the finalize assert. Conservation accounting has to cover the destruction path, not + only the payment paths. +* **Per-block work must not repeat one-shot side effects.** The workload gauge + `markets_in_dispute_window` was decremented at the call site, which now runs on every block of the + flight; it moved into the branch that runs once, when the market first enters settlement + (`payout_status != 4`). Same class of bug as the `forfeit_pool` burn in collection. + +Covered by `settle_row_budget_spans_blocks` (consensus_sim): a 140-row market settles with the +budget at its floor of 100, so it must take more than one block, must never terminate more than 100 +rows in a block, must show `payout_status = 4` in flight, and must end with every row paid, the +settlement object gone and the bettors' balances moved by exactly the sum the rows recorded. +Verified against a deliberately unbounded control — with the budget bypassed the same test reports +"a single block paid 140 rows" and "settled in 1 block", i.e. the pre-fix behaviour. + +### 4.3 Shipped: incremental void refunds + +The two void paths — cron §2 (missed resolution) and §3 (dispute auto-close) — had the same hole +with a second edge: `refund_all_bets()` walked every row of the market in one block *and* built an +in-memory vector holding every participant, because the forfeit pool is shared pro-rata and the +denominator was only known at the end of the walk. + +`refund_market_step(db, mkt, budget)` replaces both. It runs two metered passes over the same +predicate (`status` 0/5/6): pass one only measures (stake total, row count), pass two refunds the +stake and pays each row its slice of the forfeit pool. Because pass one changes nothing, pass two +re-walks *exactly* the set pass one counted — which is how "who is being refunded by this void" stays +answerable across a pause without tagging rows or holding a vector. + +The market wears `payout_status = 4` from the first block of the flight, and that flag is now a +gate, not just a display value: + +* `pm_resolve_market`, `pm_no_contest` and `pm_transfer_position` refuse it, so a late oracle call + cannot overtake a refund that is halfway through the market; +* cron §4 (dispute voting finalize) steps over disputes whose market is already being voided by §3; +* cron §6 (batch epoch settle) skips it, so queued rows cannot move between the two passes. + +One ordering bug fell out of writing this, and it predates the change: the old path drained +`forfeit_pool` *before* `return_liquidity()`, which force-closes leveraged positions and routes +their curve residual straight back **into** `forfeit_pool`. Those tokens then rode on the market row +until GC dropped it — stranded in `current_supply` with no owner, exactly the leak the void routing +exists to prevent. Liquidity is now returned first and the leftover accounted for after. + +Covered by `void_refund_row_budget_spans_blocks`: 140 rows, budget at its floor of 100, no oracle +ever resolves; the void must span blocks, stay under the budget per block, end with `status = 3`, +`resolved_outcome = -1`, every row refunded, and the bettors' balances up by exactly the stake. + +### 4.4 The batch executor (cron §6) + +Cron §6 filled every row queued into a market's current epoch inside one block, at the price of one +unit of the market-counting cap — the same shape as the settlement bug, and after fix A a queued row +costs `pm_min_bet` (1 VIZ), exactly what a bet row costs. On an LMSR market each row additionally +pays for a curve quote, making it *dearer* per row than settlement. + +It now draws on the same shared `pm_settle_rows_per_block` budget, charged per row **visited** (not +merely executed — a visit is the work the block does). Two consequences follow from the fact that +leftover rows are matched *by epoch*: + +* the epoch counter advances only once the queue is drained; bumping it mid-drain would leave the + remaining rows unreachable with their stake already debited; +* the executor therefore also runs **off** the epoch boundary while a pass is in flight + (`pm_batch_settle_bet_cursor != 0`), instead of making already-revealed stakes wait a whole epoch + window for the next boundary. + +The resume point is a second cursor in the dynamic global properties, +`pm_batch_settle_bet_cursor`, consumed by the first market the round-robin scan visits (which is +`pm_batch_settle_cursor` by construction). Losing it — e.g. an old snapshot without the field — is +safe: the pass restarts at the head of the epoch and skips the rows it already executed by status, +costing one idle walk and no money. Covered by `batch_queue_row_budget_spans_blocks`. + +One guard is load-bearing rather than cosmetic: §6 is entered only when `row_budget > 0`. It runs +last and the budget is shared, so a settle-heavy block can reach it with nothing left; entering +anyway would run zero iterations and then fall through to the persist step, which — seeing no +mid-market stop — would write a zero row cursor over the parked one. The next pass would restart at +the head of the epoch and spend budget re-visiting rows it had already executed. + +### 4.5 The deadline sweeps re-read settled markets (found 2026-08-19, fixed) + +`by_result_expiration` was keyed `(status, result_expiration, id)`. A settled market keeps +`status == 3` and its `result_expiration` stays in the past, so it sat at the **head** of the range +the §5 settle sweep walks — and skipping it costs no `cap`, so the loop never stopped early on it. +Every block therefore re-read the whole settled backlog before reaching real work: measured on the +testnet snapshot of block 82641602, **48 971 iterations of which 48 942 were pure `continue`**, with +only 29 markets actually owing a settlement. The backlog is bounded by GC retention (5 d default), +so it is not a leak — but it is proportional to turnover, and an attacker can inflate it directly by +creating and resolving markets. + +The fix keys the index `(status, finalized_time, result_expiration, id)`. `finalized_time` is +stamped exactly once, at finalization, so `finalized_time == 0` means "still owes work"; both sweeps +(§2 missed resolution, §5 settle) `lower_bound` into that group, and a market leaves it the moment +it is settled. `payout_status` is deliberately *not* in the key — the settle sweep flips it 1 → 4 +mid-flight and must not move the row it is resuming. Same trick as `by_oracle_finalized`. Index +keys are not serialized, so this needs no snapshot migration. + +### 4.6 The dispute tally (cron §4, found 2026-08-19, fixed) + +The same shape once more, in the one sweep the earlier passes never looked at. Cron §4 finalizes +disputes whose voting window closed; for each it walks **every ballot** of the disputed market to +build the stake-weighted tally, and charges the market a single unit of the market-counting `cap`. +A ballot is not a cheap row either — each one costs an account lookup plus a lazy-pool deposit +lookup, the same order as the settlement row measured in §5 below. + +M3 already caps ballots at `MAX_PM_DISPUTE_VOTES_PER_MARKET` (10 000) per market, and the comment +there reasoned that this made the finalize walk safe. It does not: the cap bounds *one* market, +while §4 may finalize `cap` of them in a block, so the ceiling was `cap × 10 000` = 2 000 000 rows — +three orders of magnitude above the budget every other sweep now respects. Filling it is slow (a +ballot needs a distinct account per market, and 200 disputes cost 200 × `pm_dispute_fee` in escrow) +but the ballots are durable state: the cost is spread over hours of chain time and the work is +replayed in the single block where the voting windows expire. + +Unlike settlement, a tally **cannot** be resumed: the verdict needs every ballot at once, and +parking the partial per-outcome sums would mean carrying a vector on the dispute row. So the budget +is enforced *between* disputes — a dispute starts only while budget is left, and is then charged for +the ballots it walked. Worst case per block becomes `row_budget` + one market's ballot cap instead +of `cap` × ballot cap. Deferring a finalize by a block is economically inert: `pm_dispute_vote` +refuses ballots past `voting_end_time`, so the electorate is already final when §4 gets there. +Covered by `dispute_tally_row_budget_defers_next`. + +The ordering property this shares with §5 and §6 is worth stating once: the budget is spent in +section order, so a block saturated by the void paths can leave nothing for the sweeps behind them. +That is deliberate — the backlogs are finite work that drains — but it means "how long until my +dispute finalizes" is bounded by the *total* PM work in flight, not by §4 alone. + +The related per-transaction cost is fixed alongside it. `pm_dispute_vote` used to enforce the ballot +cap by counting the market's existing ballots on every *new* ballot (walk bounded at cap+1): bounded +per transaction, but O(n) per ballot, O(n²) to fill a market, and work no cron budget covers — the +same antipattern M4 removed from the commit path with `open_commits`. The count now lives on +`pm_dispute_object.ballots`, incremented when a ballot row is created and left alone when a voter +*revises* one (a revision overwrites the row, so the counter tracks rows, not votes). Ballots are +never deleted individually — GC drops the whole cluster — so the counter only grows. + +Snapshots need one extra step here that `open_commits` did not. Disputes are imported *before* their +ballots, so a `contains`-guarded read of the key cannot repair a pre-field snapshot on its own: +`reconcile_pm_dispute_ballots()` runs after the ballot import and makes every counter agree with the +rows actually present. That both seeds old snapshots (key absent → 0 → rebuilt) and catches drift in +new ones, at the cost of one pass over an index the import just walked anyway. Covered by +`dispute_ballot_counter_matches_rows`, which checks the counter against a live row count after every +ballot and pins the revision path. + +### 4.7 Section order is priority order (cron §8, found 2026-08-19, fixed) + +Every section of `process_pm_markets()` charges the same counter, `done`, against the same +`pm_processing_cap_per_block`. That makes section order a priority order, which is intended for the +sweeps that do real work — but it also means a section that reliably exhausts the budget turns +everything behind it into dead code. + +Section 7, the lazy-pool recall step, is exactly such a section. It walks the status-0 allocation +index from the head every block and charges `done` for **every row it visits**, including the ones it +only inspects and leaves untouched (`idle, steps remain, but this step isn't due yet`). Charging for +inspection is deliberate — that is what keeps the section bounded — but the working set is large and +long-lived: on the testnet at block 82646702 there were **34 548** status-0 allocations against a cap +of **200**. The loop therefore always runs until `done == cap`. + +Behind it sat section 8, the ban-expiry sweep. It never executed. Temporary oracle and creator bans +kept a stale `banned_until` forever and `pm_ban_expired` was never emitted. The damage is bounded: +enforcement compares `banned_until` against `now` rather than testing the field for emptiness, so no +account stayed blocked past its term — what broke is the stored state and the history event, and any +client that reads "banned" as "field is non-zero". No ban existed on the testnet while this was true, +so nothing was observably stuck; the defect is that the section could not run at all. + +The fix gives the sweep its own counter (`ban_done`) rather than moving it or enlarging the shared +cap. That is safe because the sweep is self-clearing: a visit sets `banned_until` to 0, which drops +the row out of the swept range permanently. Per-block work is therefore the number of bans that just +expired, and the private cap bounds even a synchronised burst of them. + +`ban_expiry_survives_saturated_cron_budget` reproduces the starvation in miniature — cap 2, three +live allocations to saturate it, one short creator ban that must still lapse. With the sweep back on +the shared counter the test fails on exactly that assertion. + +The general rule this leaves behind: **a new section appended to this cron is dead on arrival unless +it either sits ahead of section 7 or carries its own budget.** + +### 4.8 The lazy-pool withdraw queue (per-tx, found 2026-08-19, fixed 2026-08-20) + +`service_lazy_withdraw_queue()` used to drain the pool's FIFO withdraw queue **in full** on every +call — it looped until `free_balance` ran out — and it is called from six places, four of them inside +evaluators (deposit, withdraw, leverage close, leverage convert) plus the two capital-return paths +in the cron. There is no floor per queue row: `pm_lazy_withdraw` creates a **new** request object on +every partial withdrawal while `owed > 0` (one raw is enough), and rows of the same account are +never merged. The asymmetry is that the queue is filled one transaction per row and drained by one +unrelated transaction later — at the testnet's 150 k VIZ of free balance a single call could pay out +up to 150 million rows. + +Fix (owner q#678=A): the drain is now budgeted. `service_lazy_withdraw_queue(db, row_limit)` returns +how many rows it processed; the per-transaction call-sites pass `1` (pay just the FIFO head — the +bulk is picked up by the cron), and the new cron section 9 drains the rest up to the shared per-block +`pm_settle_rows_per_block` row budget whenever `pending_withdrawals > 0`. That section is the +liveness backstop: the queue keeps progressing at up to `row_budget` rows per block even when no +capital returns to `free_balance`, so it cannot stall. The rows the cron pays still charge the shared +budget honestly (the return value is subtracted from `row_budget`). + +`lazy_withdraw_queue_row_budget_spans_blocks` reproduces the old behaviour in miniature — 250 one-raw +rows against a 100-row budget — and asserts the queue spans several blocks, ≤ `row_budget` per block, +FIFO order, `free_balance ≥ 0` and `pending_withdrawals → 0`. With the pre-fix unbounded drain the +control drains all 250 in the first block and fails the per-block bound. No layout change → the +testnet does not need a redeploy. + +### 4.9 The liquidation cascade runs per transaction (found 2026-08-20, open) + +Everything above bounds work **per block**. `cascade_liquidate()` breaks that frame because it is +reached from evaluators — `pm_place_bet` (both binary branches), `pm_cancel_bet` and +`pm_withdraw_liquidity` — so its cost is paid per *transaction*, and a block holds as many +transactions as it has room for. + +The scan itself is unavoidable in shape: for each round it walks the market's status-0 leverage +positions and evaluates `cancel_value()` against each one's threshold, stopping at the first victim. +When nothing is liquidatable — the normal case, and the one the comment describes as a "cheap index +probe" — it still visits **every open position on that market** before concluding there is no work. +`pm_min_bet` (1 VIZ) is all it takes to trigger one such sweep, and nothing caps how many bets a +block may carry. + +How large the swept set can grow is fixed by pool economics rather than by any explicit cap. Every +open position locks at least `pm_min_liquidity` (100 VIZ) of the leverage fund (the #536 floor), the +fund is `pm_leverage_fund_percent` of the pool's free balance, and `free_balance` itself shrinks as +loans go out, so the fixed point is roughly `N ≤ free / 1100` at today's 10 %. A pool holding ~11 M +VIZ therefore supports ~10 000 open positions, and constraint 3 caps only the size of an individual +position, not how many of them share one market. At the measured ~1.7 µs per visited row that is +~17 ms of work bought by a single 1 VIZ bet, repeated for every bet in the block. + +Worth noting where that floor came from: `pm_min_liquidity` was imposed on the loan by the #536 +audit fix, and its own comment states the intent — "bounds the global open-position count to +`fund_total / pm_min_liquidity`". That reasoning is sound for work measured **per block**, which is +what every sweep above is. It does not carry to a scan that runs once per transaction: bounding the +set says nothing about how many times the set is re-walked, and nothing caps the re-walks. + +Two honest qualifications. First, the outer loop re-scans from the head of the range after each +liquidation (`O(K·N)` for K liquidations), but K is self-damping: liquidating a position sells its +tokens back into the curve, which moves the price *toward* the remaining same-side positions and +makes them safer, so mass cascades are not the expected shape. The per-bet `O(N)` scan is the part +that does not depend on anything going wrong. Second, none of this is reachable on the testnet right +now: `pm_leverage_max_per_position_bp` (20 bp) against the current fund makes the per-position cap +(~30 VIZ) smaller than the 100 VIZ loan floor, so no position can be opened at all — the known #536 +conflict, left as-is by the owner (q#568). Resolving that conflict in favour of smaller loans would +widen this scan proportionally; the two decisions are coupled. + +**Decision (owner q#679=D, 2026-08-20): not fixing.** The lever is a niche product, the exposed set is +economically capped (fund / pool bound above), and an attacker already pays the collateral and +funding for every position the cascade has to scan. Goal #440 closed. + +### 4.10 Checked and rejected: the open-position sweep (cron §2c) + +The section that force-closes positions once betting is over walks **all** status-0 positions every +block through `by_lev_funding_due` and charges `done` only for the closes, so at first reading it +looks like §4.5 all over again: a full scan whose skipped rows cost no budget. + +It is not the same defect, and the difference is worth stating because it is the line between the two +families. In §4.5 the head of the range filled with markets that would **never** need work again, so +the idle scan grew with turnover without bound. Here every scanned row is a live obligation — an open +loan the chain must eventually close — and the row leaves the range permanently the moment it does +(status 0 → 1). The working set is therefore the same economically capped `N` as in §4.9, not a +backlog of corpses. Making it cheaper would mean storing a force-close deadline on the position and +keying an index on it: a layout change and a snapshot migration for a constant-factor win on a set +that is already bounded. Not worth it; recorded so the next audit does not re-open it. Goal #441 +closed on this reasoning. + +### 4.11 The batch executor's idle fast-path (cron §6, found 2026-08-20, fixed) + +§4.4 shipped the batch executor charging the row budget for every row it *visits*, but the **outer** +walk was still `O(active batch markets)` per boundary and not metered: an idle market (nothing queued +at the current epoch) skipped out before the LMSR q-vector snapshot and paid nothing — no `done`, no +row budget — so the per-boundary scan grew linearly with the *number of markets* regardless of how +much queue actually existed. A spammer could create thousands of `allow_batch` markets and turn every +epoch boundary into a full idle walk. + +Fix (P0): a `by_status_market` index on `pm_bet` — `(status, market, id)` — and the scan now drives +straight from queued (`status=5`) rows. Only markets that actually hold a queue are visited at all; +idle markets never enter the range. Resume semantics are unchanged: executed rows flip `5 → 0/2` and +leave the index, so the next `lower_bound((5, market, bet))` lands naturally on the next un-executed +row, and the two dynamic-properties cursors (`pm_batch_settle_cursor`, `pm_batch_settle_bet_cursor`) +keep their meaning (market / parked-row). A defensive skip steps over status-5 rows that ever leak +onto a non-active market — §2/H2 refund already pays those, and the pass must not move them between +the measuring and paying passes. Goal #445 closed. + +### 4.12 Deferred-claim purge and the phase-1/5 loud signals (2026-08-20, fixed) + +Two smaller residuals from the same review, taken together with P0 (owner chose C): + +- **P1 — `purge_deferred_claims` built an in-memory vector** of every deferred claim before deleting + it, the same participant-vector pattern `refund_all_bets` used to carry (and that §4.3 removed). + Claims are already capped by `MAX_PM_DEFERRED_CLAIMS_PER_MARKET` (10 000), so this was never a + bound problem — but the vector is free to drop: it now removes in place, advancing the iterator + before `db.remove` invalidates it. Goal #446 closed. + +- **P2 — the un-metered settlement phases 1 and 5.** `settle_market_step` budgets only phases 2–4 + (bet rows). Phase 1 (`force_close_positions`) and phase 5 (`settle_liquidity`) walk every open + position / LP row of the market whole; they are bounded only economically (each row ≥ + `pm_min_liquidity` = 100 VIZ). Rather than metering them — cursors and resume for a threat the + 100 VIZ floor already prices — the code now emits a **loud log-only signal** (mirroring the F1 + `uncovered` signal) when one of those walks overshoots `pm_settle_rows_per_block` in a single step. + It is deterministic and has no consensus effect; it exists so a future change that lowers the + collateral floor (e.g. unwinding #536 toward small loans) makes the phase visibly expensive instead + of silently degrading block time. Goal #448 closed. + +## 5. What a row actually costs + +Measured with `tests/consensus_sim/bench/settle_bench.cpp` (`make pm_settle_bench`), which drives +real markets of growing row count through a real chain and times the block that settles them. +Release build, no sanitizers, no account_history plugin: + +| rows | idle block | settling block | per row | gc block | gc per row | +|---|---|---|---|---|---| +| 500 | 0.28 ms | 1.24 ms | 1.93 µs | 0.59 ms | 0.62 µs | +| 2 000 | 0.29 ms | 3.61 ms | 1.66 µs | 1.54 ms | 0.62 µs | +| 8 000 | 0.29 ms | 13.95 ms | 1.71 µs | 5.50 ms | 0.65 µs | + +Settlement is linear in rows at **~1.7 µs/row**, garbage collection at ~0.62 µs/row. Extrapolating: +roughly **600 000 rows fill one second** of block time and ~1.7 M rows fill the whole three-second +interval. Treat that as a **lower** bound — the benchmark bets from ten accounts (a real market has +thousands, so lookups are less cache-friendly) and the simulated node runs no account_history +plugin, so the virtual operation pushed per row costs almost nothing there while an API node pays +to index it. + +That is the shape of the risk with fix A alone: an organic market is nowhere near the limit (the +734-row testnet market settles in ~1.2 ms), but a spammer who is willing to stake 1 VIZ per row can +buy ~1.7 seconds of settlement work in a single block for about a million VIZ. Cheap enough to be +worth closing, which is fix D. + +## 6. Tuning + +`pm_settle_rows_per_block` trades settlement latency against block time, and the measurement above +is what it should be set from: + +* the default **2 000** costs ~3.4 ms of settlement work per block (about 0.1 % of the interval) + and drains a million-row market in ~500 blocks, i.e. under half an hour; +* raising it to 10 000 costs ~17 ms/block and drains the same market in ~100 blocks; +* the floor of 100 exists so that progress is always guaranteed. + +Raising `pm_min_bet` shortens the queue instead, at the price of excluding small bettors — prefer +tuning the budget first. diff --git a/docs/prediction-markets/specification.md b/docs/prediction-markets/specification.md new file mode 100644 index 0000000000..f5cd6191fc --- /dev/null +++ b/docs/prediction-markets/specification.md @@ -0,0 +1,1063 @@ +# Onix Protocol Specification + +**Version:** 2.0 (on-chain / HF14) +**Status:** Formal technical specification — now realized as consensus operations on VIZ DLT + +--- + +> **On-chain (HF14).** Implemented as first-class consensus operations (`pm_*`) on VIZ DLT and verified in +> `consensus_sim`. **All percentages are basis points (bp): 10000 = 100.00%**; all durations are +> governance parameters in **seconds / blocks**. Median-voted parameters live in the `chain_properties_pm` +> struct (§3); per-market fields are the `pm_create_market` operation; all state is in the chainbase +> objects of §17. Oracle fees use offer→quote (creator ceiling → oracle freezes its quote at accept, +> emitting `pm_market_accepted`). Disputes have two modes — committee (`dispute_mode = 0`, default: +> stake-weighted **public** `pm_dispute_vote`, revisable until close, Lazy-Pool stake counts) and account +> (`dispute_mode = 1`: a named `dispute_resolver`). + +## Table of Contents + +1. [Definitions and Roles](#1-definitions-and-roles) +2. [Currency and Precision](#2-currency-and-precision) +3. [System Parameters](#3-system-parameters) +4. [Market State Machine](#4-market-state-machine) +5. [Onix Binary: Constant Product Market Maker](#5-onix-binary-constant-product-market-maker) +6. [Onix Multi: LMSR with Parimutuel Settlement](#6-onix-multi-lmsr-with-parimutuel-settlement) +7. [Fee Structure](#7-fee-structure) +8. [Time Penalty for Late Bets](#8-time-penalty-for-late-bets) +9. [Liquidity Provision](#9-liquidity-provision) +10. [Resolution and Payout](#10-resolution-and-payout) +11. [Bet Cancellation](#11-bet-cancellation) +12. [Dispute System](#12-dispute-system) +13. [Oracle Penalty for Missed Resolution](#13-oracle-penalty-for-missed-resolution) +14. [Oracle Reputation Scoring](#14-oracle-reputation-scoring) +15. [Position Transfers](#15-position-transfers) +16. [Lazy Liquidity Pool](#16-lazy-liquidity-pool) +16a. [Opt-In Leverage](#16a-opt-in-leverage-lazy-pool-funded) +16b. [Batch / Commit-Reveal Betting](#16b-batch-commit-reveal-betting-anti-mev) +17. [On-Chain Object Model](#17-on-chain-object-model) + +--- + +## 1. Definitions and Roles + +| Role | Definition | +|------|-----------| +| **Market Creator** | Pays `pm_market_creation_fee` (`pm_create_market`); sets the question, outcomes, liquidity, fee ceilings, and timing parameters | +| **Oracle** | Registers (fee: `pm_oracle_registration_fee`), deposits insurance (min: `pm_min_oracle_insurance`), quotes its fee terms in **basis points** (≤ creator ceiling) + fixed fee at acceptance, accepts/rejects markets, provides outcome decisions | +| **Bettor** | Places bets on outcomes; receives tokens proportional to stake and current reserves | +| **Liquidity Provider (LP)** | Supplies capital to market pools; earns time-weighted share of liquidity fees + penalty pool | +| **Lazy Pool Provider** | Deposits VIZ into the Lazy Liquidity Pool with a lock period; pool auto-allocates to markets and distributes rewards via `reward_per_share` accumulator | +| **Dispute Resolver** | Account mode only (`dispute_mode = 1`): the per-market `dispute_resolver` account arbitrates. Committee mode (`dispute_mode = 0`) uses no resolver — the SHARES electorate votes | +| **DAO / committee fund** | The chain's existing committee fund. Receives `pm_market_creation_fee` and extra oracle penalties | + +--- + +## 2. Currency and Precision + +All amounts are stored as integers with precision = 1/1000 (milli-VIZ). `1000` internal units = 1.000 VIZ. + +Time penalty values use precision = 1/1,000,000 (micro-units). + +--- + +## 3. System Parameters + +### Median-voted parameters (`chain_properties_pm`) + +All economic parameters are delegate median-voted (no hard fork to tune) and live in the on-chain +`chain_properties_pm` struct. Each delegate publishes its preferred values through the standard +**`versioned_chain_properties_update_operation`** (op ID 46) — `chain_properties_pm` is the current +(v5, HF14) version of that versioned struct — and the network applies the **per-field median** of the +active delegates. The two risk-coverage knobs (`pm_listing_min_coverage_percent`, +`pm_betting_min_coverage_percent`) are part of this same v5 struct and are tuned exactly the same way. +**All percentages are basis points (bp, 10000 = 100.00%); durations are seconds or blocks** — except the +two coverage knobs, which are percent-of-volume (100 = 1.0×). Exact defaults and ranges are in +[Chain Properties](../governance/chain-properties#pm-parameters); the authoritative source is the struct itself. + +| Group | Parameters | +|---|---| +| Registration & floors | `pm_oracle_registration_fee`, `pm_min_oracle_insurance`, `pm_market_creation_fee`, `pm_min_liquidity`, `pm_max_outcomes`, `pm_max_market_duration` | +| Fees & penalties (bp) | `pm_max_oracle_fee_percent`, `pm_oracle_penalty_percent`, `pm_no_contest_penalty_percent`, `pm_default_time_penalty_percent`, `pm_max_time_penalty` | +| Acceptance window | `pm_oracle_accept_window_sec` (default 3600 = 1h; pending markets not accepted/rejected within this are voided by the cron — seed refunded, creation fee kept) | +| Risk / coverage (% of volume) | `pm_listing_min_coverage_percent` (250 = 2.5×; markets covered below this are hidden from the default catalog, shown via `show_risky`), `pm_betting_min_coverage_percent` (150 = 1.5×; advisory client risk-confirm threshold, `≤` the listing one, not enforced on-chain) | +| Disputes | `pm_dispute_fee`, `pm_dispute_grace_sec`, `pm_oracle_dispute_response_sec`, `pm_dispute_vote_period_sec`, `pm_dispute_auto_close_sec`, `pm_dispute_approve_min_percent` (bp), `pm_dispute_reward_multiplier` (bp) | +| Lazy pool | `pm_lazy_pool_enabled`, `pm_lazy_alloc_percent`, `pm_lazy_max_total_alloc_percent`, `pm_lazy_recall_step_percent`, `pm_lazy_lock_sec`, `pm_lazy_emergency_penalty_percent`, `pm_lazy_min_liquidity_fee_percent` (default 200 = 2%; pool skips markets whose `liquidity_fee_percent` is below this reward floor) | +| Leverage | `pm_leverage_enabled`, `pm_leverage_fund_percent`, `pm_leverage_max_per_position_bp`, `pm_leverage_max_position_ratio_percent`, `pm_leverage_min_market_liquidity`, `pm_leverage_safety_margin_percent`, `pm_leverage_max_slippage_percent`, `pm_leverage_m_factor_percent`, `pm_leverage_pool_profit_percent`, `pm_leverage_expiration_buffer_sec`, `pm_conversion_profit_cost_percent` | +| Batch / commit-reveal | `pm_commit_reveal_enabled`, `pm_batch_epoch_blocks`, `pm_reveal_window_blocks`, `pm_commit_no_reveal_penalty_percent` (bp), `pm_min_batch_bet` | +| Processing | `pm_processing_cap_per_block` | + +The recipient of `pm_market_creation_fee` and extra oracle penalties is the chain's existing committee/DAO +fund — not a PM-specific account. + +### Per-market parameters (`pm_create_market` operation) + +Set by the creator at creation; the oracle fee fields are a **ceiling** the oracle quotes against at +acceptance (offer→quote). Full field reference: [Prediction Market operations](../protocol/operations/prediction-markets). + +| Field | Description | +|---|---| +| `oracle`, `market_type` (0 binary / 1 multi), `outcomes`, `url` | market definition | +| `oracle_fee_percent`, `oracle_fixed_fee` | oracle fee **ceiling** (bp + fixed); the oracle freezes its quote ≤ this (and ≤ median `pm_max_oracle_fee_percent`) at accept | +| `creator_fee_percent`, `liquidity_fee_percent` | creator & LP fees (bp of the losers' pool) | +| `liquidity`, `lmsr_b` | seed liquidity; `lmsr_b` for multi markets | +| `betting_expiration`, `result_expiration` | timers | +| `time_penalty_type`, `time_penalty_value`, `penalty_curve_type` | late-bet penalty shape | +| `allow_early_resolution`, `allow_cancellation` | toggles | +| `allow_batch`, `allow_instant_bet` | betting modes (binary) | +| `endogeneity_tier` | 1 econ-data / 2 sports / 3 political (display/risk hint) | +| `dispute_mode` (0 committee / 1 account), `dispute_resolver` | dispute routing | +| `dispute_penalty_percent` | oracle penalty policy on a successful dispute (bp, signed) | +| `metadata` | free-form client JSON (consensus-opaque; parsed off-chain) | + +--- + +## 4. Market State Machine + +### States + +| Status | Name | Description | +|--------|------|-------------| +| -1 | Deleted | Oracle rejected, **or** the acceptance window (`pm_oracle_accept_window_sec`) expired; seed liquidity returned to creator (creation fee kept) | +| 0 | Waiting | Awaiting oracle review | +| 1 | Active | Accepting bets until `betting_expiration` | +| 2 | Closed | Betting ended, awaiting oracle resolution | +| 3 | Resolved | Outcome determined, payouts calculated | + +### Payout States + +| payout_status | Name | Description | +|---------------|------|-------------| +| 0 | Not calculated | Pre-resolution | +| 1 | Calculated | Payouts pending (grace period active) | +| 2 | Paid | All payouts processed | +| 3 | Disputed | Dispute filed, payouts frozen | + +### Transitions + +```mermaid +stateDiagram-v2 + direction LR + state "Waiting (0)" as Waiting + state "Active (1)" as Active + state "Closed (2)" as Closed + state "Resolved (3)" as Resolved + state "Deleted (-1)" as Deleted + state "Paid out" as Paid + [*] --> Waiting + Waiting --> Active: oracle accepts + Waiting --> Deleted: oracle rejects + Waiting --> Deleted: accept window expires (pm_market_expired) + Active --> Closed: betting_expiration + Active --> Resolved: early resolution (if allowed) + Closed --> Resolved: oracle resolves + Resolved --> Paid: grace period (12h), no dispute + Deleted --> [*] + Paid --> [*] +``` + +**Preconditions:** + +| Transition | Preconditions | +|-----------|---------------| +| 0 → 1 | Oracle has insurance ≥ `min_oracle_insurance`; oracle accepts | +| 0 → 1 (self-oracle) | Creator = oracle; insurance check; auto-approves at creation | +| 0 → -1 | Oracle rejects; seed liquidity returned to creator | +| 0 → -1 (expiry) | `now ≥ created_time + pm_oracle_accept_window_sec` with no oracle action; cron voids the market, refunds the seed (creation fee kept), emits `pm_market_expired` | +| 1 → 3 | Oracle submits resolution with outcome (0, 1, or -1 for no-contest); `allow_early_resolution=1` or `time ≥ betting_expiration`. Resolution sets `result_expiration = now` **unconditionally** (early *or* late, mirrors no-contest), so the dispute-grace / settle / LP-lock schedule is always anchored to the announcement — disputers get the full `pm_dispute_grace_sec` window whether the report was early or late (a late report can't grief them by burning the window) | +| 2 → 3 | Oracle submits resolution; `time ≤ result_expiration` | +| 3 → paid | Grace period passed with no dispute; cron processes payouts | + +### Market Creation Flow + +1. Deduct `market_creation_fee` from creator → DAO fund (non-refundable) +2. Record `oracle_fixed_fee` from oracle profile on market +3. Lock `liquidity` from creator balance +4. Initialize reserves: `reserve_a = floor(liquidity/2)`, `reserve_b = liquidity − reserve_a` +5. Compute `k = reserve_a × reserve_b` +6. If self-oracle: auto-approve to status=1 with insurance check +7. If external oracle: enter status=0 and set `accept_deadline = created_time + pm_oracle_accept_window_sec` + +### Oracle Acceptance Flow + +A pending market must be resolved by its oracle within the acceptance window +(`pm_oracle_accept_window_sec`, default 1h). Three outcomes: + +- **Accept** (status 0 → 1): (1) transfer `oracle_fixed_fee` from creator to oracle (skipped if + self-oracle); (2) increment oracle `markets_accepted`; (3) update `last_active_time`; (4) trigger + Lazy Pool auto-allocation (if the pool has free balance **and** the market's `liquidity_fee_percent + ≥ pm_lazy_min_liquidity_fee_percent`). +- **Reject** (status 0 → -1): seed liquidity refunded to creator; no vop. +- **Expiry** (status 0 → -1): if neither happens by `accept_deadline`, the per-block cron voids the + market, refunds the seed liquidity (**not** the creation fee), and emits `pm_market_expired`. + +### Audit Trail + +Every state-changing action is a consensus operation or virtual operation, permanently recorded in the block log and queryable via `account_history`. Bets, cancels, liquidity add/withdraw, accept/reject, resolution, dispute, dispute-resolve, payout, and penalty all appear as `pm_*` operations/virtual-operations, alongside the market reserves they touched. + +--- + +## 5. Onix Binary: Constant Product Market Maker + +### Invariant + +``` +k = reserve_a × reserve_b +``` + +`k` changes only on liquidity add/withdraw operations. + +### Bet Placement (side A) + +``` +new_reserve_b = reserve_b + amount +new_reserve_a = floor(k / new_reserve_b) +tokens_received = reserve_a − new_reserve_a +price = amount × 1,000,000 / tokens_received +``` + +Symmetric for side B (swap a/b). + +### Slippage Protection + +Optional `min_tokens` parameter on `place-bet`. If `tokens_received < min_tokens`, transaction rejected. + +### Market Initialization + +``` +reserve_a = floor(liquidity / 2) +reserve_b = liquidity − reserve_a +k = reserve_a × reserve_b +``` + +Minimum initial liquidity: 100,000 mVIZ (100 VIZ). + +### Weight (Token) Semantics + +- `weight` = number of outcome tokens received by bettor (set by the CPMM at bet time) +- `weight` is a **relative claim**, not a VIZ-denominated payout. Settlement is **parimutuel** (identical to Onix Multi): winners receive their stake back plus a proportional share of the losers' pool, by weight. +- If bet on side A and outcome A wins: `payout = bet_amount + (weight / total_winning_weight) × winners_pool − time_penalty_on_profit` +- If outcome A loses: payout = 0 (stake forfeited into the winners' pool) + +CPMM is the **pricing engine** (probability + weight assignment); it no longer gates payout. This makes the two market types share one settlement model: *the AMM assigns weights (CPMM for binary, LMSR for multi); losers fund winners pro-rata by weight.* + +### Price Display + +``` +implied_probability_A = reserve_b / (reserve_a + reserve_b) × 100% +implied_probability_B = reserve_a / (reserve_a + reserve_b) × 100% +``` + +### LP Principal Guarantee (Proof) + +Under parimutuel settlement the guarantee is exact and does not rely on the curve geometry: + +``` +Money OUT = L (LP principal) + Σ(winning bet_amount) + winners_pool + fees + = L + winning_bets + (losers_sum − fees) + fees + = L + winning_bets + losing_bets = L + all_bets = Money IN +``` + +Total payout is capped at `losers_sum` regardless of weights, so LP principal `L` is returned unconditionally and winners are funded entirely by losers. (The legacy AM-GM bound `reserve_a + reserve_b ≥ L` is no longer needed for solvency; it remains a property of the pricing curve.) + +--- + +## 6. Onix Multi: LMSR with Parimutuel Settlement + +### Price Function (Softmax) + +For N outcomes with quantity parameters q_1, ..., q_N and liquidity parameter b: + +``` +price(i) = exp(q_i / b) / Σ_j exp(q_j / b) +``` + +**Invariant:** `Σ_i price(i) = 1` (by definition of softmax). + +### Cost Function + +``` +C(q) = b × ln(Σ_j exp(q_j / b)) +``` + +Cost to buy Δ tokens on outcome i: + +``` +cost = C(q + Δ·e_i) − C(q) + = b × [ln(Σ_j exp(q'_j / b)) − ln(Σ_j exp(q_j / b))] +where q'_i = q_i + Δ, all other q'_j = q_j +``` + +Numerical stability (log-sum-exp trick): + +``` +ln(Σ exp(x_j)) = max(x) + ln(Σ exp(x_j − max(x))) +``` + +### Liquidity Parameter + +``` +b = S / ln(N) +``` + +where S = LP subsidy deposit, N = number of outcomes. + +### Settlement (at resolution) + +``` +1. Oracle declares winning outcome +2. losers_sum = Σ bet_amount for all non-winning bets +3. oracle_fee = floor(losers_sum × oracle_fee_percent / 10000) +4. creator_fee = floor(losers_sum × creator_fee_percent / 10000) +5. liq_fee = floor(losers_sum × liquidity_fee_percent / 10000) +6. winners_pool = losers_sum − oracle_fee − creator_fee − liq_fee +7. For each winning bettor: + payout = bet_amount + (their_tokens / total_winning_tokens × winners_pool) − time_penalty +8. LP subsidy returned unconditionally +9. LP earns time-weighted share of liq_fee +``` + +### LP Principal Guarantee (Proof) + +1. LP deposits S VIZ as subsidy. This sets b = S / ln(N). +2. During betting, users pay VIZ → receive tokens. VIZ accumulates as betting pool. +3. At resolution: losers forfeit 100% → `losers_sum`. Winners paid from `losers_sum` (not from subsidy). +4. LP subsidy S returned **unconditionally** — it is architecturally separate from the payout flow. + +### Edge Cases + +| Scenario | Outcome | +|----------|---------| +| All bets on winning outcome | `losers_sum=0`, `winners_pool=0`. Every bettor gets back `bet_amount`. LP subsidy returned. | +| No bets on winning outcome | `losers_sum=total_bets`. Undistributed `winners_pool` → LP bonus. | +| Zero-volume market | LP subsidy returned. No fees, no payouts. | +| Single bettor wins | Bettor receives `bet_amount + winners_pool`. LP subsidy returned. | + +### Operations + +| Operation | Description | +|-----------|-------------| +| `pm_create_market_multi { oracle, outcomes, liquidity, fees, ... }` | Create N-outcome market | +| `pm_place_bet_multi { market, outcome_index, amount, min_tokens }` | Buy tokens for outcome | +| `pm_cancel_bet_multi { bet_id, min_return }` | Sell tokens back via reverse LMSR | +| `pm_add_liquidity_multi { market, amount }` | Add LP subsidy (increases b) | +| `pm_withdraw_liquidity_multi { liquidity_id }` | Withdraw LP subsidy (min floor enforced) | +| `pm_resolve_multi { market, winning_outcome }` | Oracle declares winner, triggers settlement | + +Binary markets (N=2) use Onix Binary (CPMM). LMSR used only for N > 2. + +--- + +## 7. Fee Structure + +### Resolution-Time Fee Computation + +All percentage fees computed at resolution from **losing side's total volume**: + +``` +losers_sum = Σ bet_amount for all losing bets + +oracle_fee = floor(losers_sum × oracle_fee_percent / 10000) +creator_fee = floor(losers_sum × creator_fee_percent / 10000) +liquidity_fee = floor(losers_sum × liquidity_fee_percent / 10000) +winners_pool = losers_sum − oracle_fee − creator_fee − liquidity_fee +``` + +Fees are NOT deducted from bets at placement time. Full bet amount enters CPMM/LMSR reserves. + +### Oracle Fixed Fee + +One-time fee per market. Set by oracle on profile. Paid by creator to oracle at market acceptance. Skipped entirely for self-oracle markets (no balance operation occurs). + +### Fee Tracking Fields + +- `oracle_fee_earned` — not used at resolution; fee computed from losers_sum +- `liquidity_fee_earned` — total LP commission actually paid out at settlement (a post-hoc counter written in `settle_liquidity`; 0 before that and on void markets). Early-exit LP withdrawals earn no commission: `pm_withdraw_liquidity` requires `status == 0`, while `earned_fee` is assigned together with `status = 3` at settlement, so the withdrawal path can never observe it. At settlement the LP bonus is `floor(losers_sum × liquidity_fee_percent / 10000) + penalty_pool + undistributed dust` (§7), split across active LPs by amount·time. +- Per-bet `oracle_fee` and `liquidity_fee` recorded for audit; not accumulated on market + +### Rounding + +All calculations use `floor()`. Undistributed dust (< 1 mVIZ) sent to DAO fund during final payout. + +--- + +## 8. Time Penalty for Late Bets + +### Penalty Window + +| Type | Window Calculation | +|------|-------------------| +| Fixed (type=0) | `penalty_window = time_penalty_value` seconds before expiration | +| Percentage (type=1) | `penalty_window = time_penalty_value / 100 × (betting_expiration − market_creation_time)` | + +### Penalty Calculation + +``` +time_to_expiration = betting_expiration − current_time + +if time_to_expiration < penalty_window: + ratio = 1 − (time_to_expiration / penalty_window) + + if penalty_curve_type == 1: // quadratic + penalty_ratio = ratio × ratio + else: // linear + penalty_ratio = ratio + + time_penalty = floor(penalty_ratio × max_time_penalty) +else: + time_penalty = 0 +``` + +### Application at Payout (Profit-Only) + +``` +profit = floor(winners_pool × weight / total_winning_weight) // parimutuel share of losers' pool +penalty_deduction = floor(profit × time_penalty / 1,000,000) +net_payout = bet_amount + profit − penalty_deduction +``` + +**Invariant:** `net_payout ≥ bet_amount` — the penalty applies only to the profit share, so winners always receive at least their principal. (Identical for Onix Binary and Onix Multi.) + +### Risk-Time Source + +`time_penalty` is computed once, when the bet is created, from the moment the market exposure was +actually taken — not from later mechanics: + +| Path | `risk_time` used | +|------|------------------| +| Instant / direct batch bet (`pm_place_bet`) | placement time | +| Commit–reveal (`pm_reveal_bet`) | the **commit** time (blind), so revealing late within the window is not penalised | +| Converted leverage position (`pm_leverage_convert`) | the leverage **open** time (opens are gated ≥24 h before expiry) | +| Transferred position (`pm_transfer_position`) | **inherited** from the source bet | + +A market with `time_penalty_value = 0` (the default) has no window — the penalty is always 0 and the +mechanism is dormant until a creator opts in. Open-ended markets (no `betting_expiration`) never apply it. + +--- + +## 9. Liquidity Provision + +### Adding Liquidity + +``` +// Price-neutral: scale both reserves by (L + amount) / L, where L = liquidity_sum +// BEFORE the deposit. The reserve ratio (the odds) is unchanged — only depth grows, +// linearly with capital (k scales by ((L+amount)/L)²). A first deposit into an empty +// curve (L = 0) seeds a balanced 50/50 split, as at market genesis. +factor = (liquidity_sum + amount) / liquidity_sum +new_reserve_a = floor(reserve_a × factor) +new_reserve_b = floor(reserve_b × factor) +new_k = new_reserve_a × new_reserve_b +``` + +Records `sec_to_expiration = betting_expiration − current_time` at deposit time. + +### Time-Weighted Fee Distribution (at resolution) + +``` +fee_pool = remaining_liquidity_fee + total_penalty_pool + +weight_i = amount_i × max(1, sec_to_expiration_i) +total_weight = Σ weight_i +fee_share_i = floor(fee_pool × weight_i / total_weight) +lp_payout_i = principal_i + fee_share_i +``` + +Each deposit is an independent position. Multiple deposits by the same user are tracked separately. + +### Early Withdrawal + +**Preconditions:** market live (not resolved) and betting still open (`time < betting_expiration`, or an open-ended market with no deadline); `resulting liquidity_sum ≥ pm_min_liquidity`. Once betting closes — or, for an open-ended market, once the oracle resolves — the position is locked until **settlement** (not merely resolution): the principal backs the pending F1 shortfall charge until `finalized_time` is stamped by the auto-payout / void / expiry sweep (see below). + +**Leverage-aware withdraw floor (audit #2):** while **any** leverage position on the market is still open, the withdraw floor is raised from `pm_min_liquidity` (~100) to `pm_leverage_min_market_liquidity` (~5000). Outstanding pool loans are backed by curve depth; an unchecked withdraw could shrink the curve below what the loans require and leave the pool holding under-collateralized positions. As a backstop, the liquidation cascade is re-run after a CPMM withdraw to catch individual positions squeezed by the reduced depth. + +``` +// Price-neutral: shrink both reserves by (L − withdraw_amount) / L, where L = +// liquidity_sum BEFORE the withdrawal. Mirror of the proportional add — the reserve +// ratio (the odds) is unchanged, depth falls with capital, and an add→withdraw +// round-trip of the same amount leaves the curve untouched. +factor = (liquidity_sum − withdraw_amount) / liquidity_sum +new_reserve_a = floor(reserve_a × factor) +new_reserve_b = floor(reserve_b × factor) +new_k = new_reserve_a × new_reserve_b + +// Time-ratio discount +time_served = current_time − lp_deposit_time +market_duration = betting_expiration − market_creation_time +time_ratio = min(1, time_served / market_duration) + +// Fee share (conservative: min of both sides) +fee_from_a = floor(a_bets_sum × liquidity_fee_percent / 10000) +fee_from_b = floor(b_bets_sum × liquidity_fee_percent / 10000) +estimated_pool = min(fee_from_a, fee_from_b) − already_paid_to_early_lps +lp_tw = withdraw_amount × max(1, sec_to_expiration) +total_tw = Σ (active LP time-weights) +raw_fee_share = floor(estimated_pool × lp_tw / total_tw) +fee_share = floor(raw_fee_share × time_ratio) + +returned = withdraw_amount + fee_share +``` + +**Post-expiration lock:** LP withdrawal blocked when `time ≥ betting_expiration`. All LP positions locked until resolution. + +### Principal Safety on Early Withdrawal + +Withdrawal subtracts original `weight_a` and `weight_b` (not proportional share of current reserves). If `reserve_a < weight_a` or `reserve_b < weight_b`, withdrawal is **blocked**. + +### Creator as First LP + +Market creator is automatically the first LP. Their `sec_to_expiration` equals the full market duration, giving maximum time-weight. + +--- + +## 10. Resolution and Payout + +### Payout Priority Order + +| Priority | Type | Recipient | Amount | +|----------|------|-----------|--------| +| 1 | Oracle fee (2) | Oracle | `floor(losers_sum × oracle_fee_percent / 10000)` | +| 1.5 | Creator fee (7) | Creator | `floor(losers_sum × creator_fee_percent / 10000)` | +| 2 | Creator LP (1) | Creator | `principal + time-weighted fee share` | +| 3 | LP return (1) | LPs | `principal + time-weighted fee share` | +| 4 | Winner bets (0) | Winners | `bet_amount + floor(winners_pool × weight / total_winning_weight) − penalty_deduction` (parimutuel) | +| 5 | Dispute refund (5) | Dispute participants | (if applicable) | +| 6 | Oracle penalty bonus (6) | All participants | (if oracle penalized) | + +### Losing Side + +Payout = 0. Stakes absorbed into reserve pool. + +### Zero-Volume Markets + +LPs receive full principal. Oracle receives fixed fee (if any). All fee accumulators remain 0. + +--- + +## 11. Bet Cancellation + +### Preconditions + +| Condition | Check | +|-----------|-------| +| Bet is active | `bet.status == 0` | +| User owns bet | `bet.user == current_user.id` | +| Market active | `market.status == 1` | +| Betting open | `current_time < market.betting_expiration` | +| Cancellation allowed | `market.allow_cancellation == 1` | + +### Reverse CPMM Mechanics + +For bet on side A (side=0): + +``` +new_reserve_a = reserve_a + tokens +new_reserve_b = floor(k / new_reserve_a) +amount_returned = reserve_b − new_reserve_b +if amount_returned <= 0: amount_returned = 0 +``` + +Symmetric for side B. + +**Depth-normalized pricing (audit #1-C):** the refund/tail split is computed on the reserves scaled to the bet's *entry* depth — reserves are multiplied by `entry_liquidity / liquidity_sum` (recorded in `pm_bet_object.entry_liquidity`), then clamped so normalization may only *reduce* the payout. This closes the self-liquidity vector (bet → own `add_liquidity` inflates curve depth → larger `curve_refund`) without opening a shrink-side one. See `early-exit-deferred-claim.md`. + +### Slippage Protection + +Optional `min_return` parameter. If `amount_returned < min_return`, transaction rejected. + +### State Changes (atomic) + +1. Bet status → 1 (cancelled), `returned_amount` recorded +2. Market reserves updated +3. Market bet sums decremented by original bet amount +4. User balance increased by `amount_returned`, `bets_balance` decreased +5. History entry (type=4) logged +6. Market log entry with before/after reserves + +--- + +## 12. Dispute System + +### Filing Preconditions + +- Filer has placed a bet on the market +- Within `pm_dispute_grace_sec` after resolution +- Routing by mode: **committee** (`dispute_mode = 0`) — no resolver account needed, the SHARES electorate votes via `pm_dispute_vote` (public, revisable until `voting_end_time`, weight = `effective_vesting_shares` + Lazy-Pool stake→shares), tallied by the `pm_dispute_finalize` cron; **account** (`dispute_mode = 1`) — the market's named `dispute_resolver` issues `pm_dispute_resolve` +- No open dispute on market +- Filer pays `pm_dispute_fee` + +### Oracle Response + +Mandatory within `pm_oracle_dispute_response_sec`. If missed, `pm_dispute_fee` is auto-slashed from insurance and recorded on the oracle object. + +The oracle posts its rebuttal with **`pm_dispute_oracle_respond`** (op ID 98). Because a dispute is an open public hearing, the text is stored **on the dispute object** (`oracle_response` + `oracle_response_time`, readable via `get_dispute`) so every committee voter or account resolver can weigh it before deciding. Only the market's oracle may respond, only while the dispute is open and `now ≤ oracle_response_deadline`; re-posting overwrites the previous response. + +### Dispute Lifecycle + +``` +Resolution (T=0) → Grace period (T to T+12h) → Dispute filed (T≤12h) + → Oracle response (12h window) → Resolver decision (up to 14 days) + → After verdict: recalculate or unfreeze → Auto-payout after new grace period + → Auto-close fallback (T+14 days): full refund + oracle penalty +``` + +### Dispute Upheld (Oracle Wrong — overturned) + +The disputer's reward is a carve-out of the slash; the **rest of the slash funds the winning bettors** +(via `forfeit_pool`), not a resolver and not the DAO. **Neither the committee voters nor the account-mode +resolver are paid** — voting/resolving is an unpaid duty. + +``` +reward_target = floor(dispute_fee × pm_dispute_reward_multiplier / 10000) // bp; 30000 = ×3 +bonus = max(0, reward_target − dispute_fee), capped at the slash + +1. Disputer ← dispute_fee (escrow returned) + bonus // bonus drawn from the slashed insurance +2. forfeit_pool += (slash − bonus) // → winners, through winners_pool at settlement +``` + +Slash size: committee mode scales the oracle's `dispute_penalty_percent` by `consensus_strength`; account +mode uses the resolver's `penalty_amount` (both capped at the remaining insurance). + +### Dispute Rejected (Oracle Right — upheld) + +``` +Disputer forfeits the whole dispute_fee → oracle (100%, compensation). // no resolver/DAO split +``` + +### Recalculation Process (Oracle Wrong) + +1. Validate penalty (capped at remaining insurance) +2. Disputer reward (fee + bonus) paid; slash remainder → `forfeit_pool` (winners) +3. Oracle insurance slashed +5. Bans applied (if requested) +6. Delete all existing non-paid payouts +7. Flip winning outcome (A↔B) +8. Regenerate payouts from scratch with corrected outcome +9. Audit trail recorded + +### Resolver Powers — bans are a compliance/regulator feature (account mode only) + +The sanctions below are fields of the **account-mode** `pm_dispute_resolve` (op ID 80), issued by the +market's named `dispute_resolver`. When that resolver is a **regulator or a licensed arbitrator**, this is +how it enforces off-chain rules: in a single verdict it can slash insurance **and bar both the oracle and +the market creator** from the platform, temporarily or permanently. **Committee/DAO mode +(`dispute_mode = 0`) has no ban power by design** — a public hearing only slashes insurance (scaled by +consensus strength) and adjusts reputation via `pm_dispute_finalize`; it never bans. + +| Parameter | Type | Description | +|-----------|------|-------------| +| `penalty_amount` | mVIZ | Additional insurance slash (0 to remaining) → DAO fund | +| `ban_oracle` | 0/1 | Ban oracle | +| `ban_oracle_until` | unix ts / 0 | 0=permanent, >0=expires | +| `ban_creator` | 0/1 | Ban creator | +| `ban_creator_until` | unix ts / 0 | 0=permanent, >0=expires | + +A ban records the issuing `resolver` in the target's `banned_by`. **Lifting a ban:** the same resolver +may lift it **early** with **`pm_unban`** (op ID 99, `unban_oracle` / `unban_creator`); otherwise it simply +lapses at `banned_until`, at which point the per-block cron clears it and emits the **`pm_ban_expired`** +virtual op (ID 100) so history/indexers observe the lift. + +### Auto-Close (14-day fallback) + +| Action | Description | +|--------|-------------| +| Plaintiff | Dispute fee refunded | +| Oracle | `dispute_fee` slashed from insurance | +| Bets | All refunded (original amounts) | +| LPs | All refunded (principal only) | +| Penalty distribution | Slashed amount distributed proportionally to all participants | +| Dispute status | Set to 3 (auto-closed) | + +### No-Contest Declaration + +Oracle calls `oracle-no-contest` with `market_id` and `reason`. + +1. All bets → pending refund payouts (full original amount) +2. All LP positions → pending refund payouts (principal only) +3. Penalty: `oracle_no_contest_penalty_percent`% of `dispute_fee` from insurance +4. Penalty distributed proportionally to participants +5. Market: `resolved_outcome = -1`, `payout_status = 1` +6. Grace period starts (disputable) + +**`forfeit_pool` routing on void (audit #5):** any accumulated `forfeit_pool` (commit-forfeits, leverage-exit residual) is **not** zeroed into nowhere — that would orphan real tokens in `current_supply` (a growing conservation deficit / replay-halt risk). It is routed like the `winners_pool ≥ 0` path: if bettors are present it is paid **pro-rata by stake** (`adjust_balance`); if none, it is **burned from supply** (`burn_asset`). Token conservation holds on the void path. + +### 3-Outcome Resolution (No-Contest Dispute) + +Resolver chooses one of: +- `correct_outcome = 0` — A wins (recalculate payouts) +- `correct_outcome = 1` — B wins (recalculate payouts) +- `correct_outcome = -1` — Confirm no-contest (keep refund payouts) + +If oracle wrong: pending refund payouts deleted, replaced with correct winner payouts. Standard dispute penalties apply. + +--- + +## 13. Oracle Penalty for Missed Resolution + +If the oracle still has not reported by `result_expiration + pm_dispute_grace_sec` (a resolution +grace: the missed-resolution cron voids only past this cutoff, so a fixed-deadline market stays +resolvable *at* its deadline instead of being voided one block before any resolve transaction's +clock can reach it): + +``` +penalty_amount = floor(oracle_insurance × oracle_penalty_percent / 100) +``` + +### Distribution + +``` +stakes[user_id] += bet_amount (for each active bet) +stakes[user_id] += liquidity_amount (for each active LP position) +total_stakes = Σ stakes[user_id] + +bonus_i = floor(penalty_amount × stakes[user_id] / total_stakes) +``` + +Each participant receives: full refund (principal) + proportional bonus. + +Market finalized: status=3, payout_status=2. + +### Insurance withdrawal gate (audit #3) + +An oracle may **top up** its insurance at any time, but may **not withdraw** below `pm_min_oracle_insurance` while it still carries an **open obligation** it could be slashed on. Otherwise insurance is theater: an oracle could withdraw it to the minimum right before a deterministic slash (missed resolution / dispute loss), leaving nothing to slash. + +The slashable set is defined by the invariant `finalized_time == 0` — a market still awaiting resolution (status 1) **or** resolved-but-unsettled / within the dispute-grace window (status 3, `finalized_time` not yet stamped). Checking only *already-open* disputes would miss the resolve → withdraw → dispute race (resolve a lone market, withdraw while no dispute is filed yet, then lose a dispute against emptied insurance); gating on `finalized_time` subsumes it — an oracle stays bonded until every market it took is fully **settled**, not merely resolved. Computed-on-read via the `(oracle, finalized_time, id)` index (no stored counter, no migration/drift); status-0 pending-accept markets are skipped, so a creator cannot grief-lock an oracle's insurance with sham markets. + +--- + +## 14. Oracle Reputation Scoring + +### Raw Metrics (stored on `pm_oracle_object`) + +Reputation counters and telemetry (exact field names as reflected on the object): + +| Field | Type | Source | +|--------|------|--------| +| `markets_accepted` | counter | oracle-accept-market | +| `markets_resolved` | counter | resolve-market | +| `no_contest_count` | counter | oracle-no-contest | +| `missed_count` | counter | cron (missed deadline) | +| `disputes_received` | counter | create-dispute | +| `disputes_lost` | counter | resolve-dispute (status=1) | +| `disputes_won` | counter | resolve-dispute (status=2) | +| `disputes_auto_closed` | counter | cron (14-day auto-close) | +| `dispute_responses_missed` | counter | cron (12 h response deadline) | +| `total_volume_resolved` | mVIZ | resolve-market (Σ `bets_sum`) | +| `total_insurance_slashed` | mVIZ | all penalty events | +| `avg_resolution_time` | seconds | resolve-market (running mean of betting-close→resolve latency) | +| `resolved_late_count` | counter | resolve-market (`now > result_expiration`) — feeds `timely` | +| `resolution_time_hist` | 8×counter | resolve-market (latency histogram, guaranteed zero-init `fc::array`) | +| `penalty_stamps` | counter | slash events (decays in the score) | +| `last_penalty_stamp_time` | timestamp | most recent stamp (10-day decay anchor) | +| `bans_received` | counter | resolve-dispute | +| `active_markets` | gauge | O(1) count of `status 1` markets | +| `markets_in_dispute_window` | gauge | resolved+disputable markets (O(1)) | +| `disputes_awaiting_response` | gauge | open disputes not yet answered (O(1)) | +| `disputes_awaiting_decision` | gauge | open disputes answered, pending decision (O(1)) | +| `active_since` | timestamp | register-oracle | +| `last_active_time` | timestamp | accept/resolve/no-contest | + +The four gauges are seeded once on the upgrade block, then maintained incrementally at every transition; +they are display-only and never gate consensus. See §14 → *Workload Gauges & Latency Telemetry*. + +### Derived Rates + +Denominator: `total_outcomes = markets_resolved + no_contest_count + missed_count` + +| Rate | Formula | +|------|---------| +| `resolution_rate` | `markets_resolved / total_outcomes` | +| `dispute_loss_rate` | `disputes_lost / disputes_received` | +| `no_contest_rate` | `no_contest_count / total_outcomes` | +| `deadline_miss_rate` | `missed_count / total_outcomes` | +| `dispute_response_rate` | `1 − (dispute_responses_missed / disputes_received)` | +| `on_time_rate` | `(markets_resolved − resolved_late_count) / markets_resolved` | + +### Reliability Score (v2.1 — basis points, 0–10000) + +The shipped score is a **non-consensus API heuristic** (display only — it never gates consensus, so +its weights are freely tunable). It is computed on read in `reliability_score()` (the +`prediction_market_api` plugin), **not stored**, and returned on the `pm_oracle` DTO from `get_oracle`. +It blends four reputation ratios, docks time-decayed penalty stamps and bans, then confidence-shrinks a +thin track record toward a neutral prior: + +``` +accuracy = markets_resolved / (markets_resolved + missed_count) // resolved vs missed-deadline +verdicts = disputes_won / (disputes_won + disputes_lost) // dispute outcomes upheld +responsive = (disputes_received − dispute_responses_missed) / disputes_received +timely = (markets_resolved − resolved_late_count) / markets_resolved // on-time vs past-deadline resolves + +score = accuracy·40% + verdicts·30% + responsive·15% + timely·15% // each ratio in bp [0..10000] +score −= penalty_stamps × 300 bp // halved per 10 days since last_penalty_stamp_time (decay, ≤16 halvings) +score −= bans_received × 1500 bp +if markets_resolved < 20: // unproven oracle: neither a hard 100 nor a crater on one dispute + score = (score × n + 6000 × (20 − n)) / 20 // n = markets_resolved; shrink toward 6000 prior +score = clamp(0, 10000, score) +``` + +Each ratio defaults to a full `10000` until the oracle has the relevant history (optimistic when +unproven). **`timely`** is the P5 lateness signal (variant A): a late-but-delivered resolve still +increments `markets_resolved` — so it earns full `accuracy` credit — and `timely` is what separates a +chronically-late oracle from a punctual one (max −1500 bp for an always-late oracle at 15% weight). +`avg_resolution_time` is deliberately **not** scored: it measures latency from betting close, not +deadline overrun, and cannot be normalized without the market length. Weight defaults: + +| Component | Weight | +|-----------|--------| +| `accuracy` | 40% | +| `verdicts` | 30% | +| `responsive` | 15% | +| `timely` | 15% | +| penalty stamp | −300 bp each (10-day half-life) | +| ban | −1500 bp each | +| confidence prior | 6000 bp until `markets_resolved` ≥ 20 | + +Clients render the score as `reliability_score / 100` (percent). v1 (used through early testnet) blended +only the success and dispute-win ratios minus a ban dock; v2 added responsiveness, decayed penalty +stamps and confidence shrink; v2.1 added the `timely` factor and rebalanced the weights. + +### Workload Gauges & Latency Telemetry + +Per-oracle operational metrics exposed via `get_oracle` (and drill-down list methods) so an Oracle +Console shows what an oracle owes *now* without paging its whole market set. Also non-consensus. + +**Stored gauges** on `pm_oracle_object` — maintained O(1) at every state transition (op- and cron-driven) +and seeded once on the upgrade block: + +| Gauge | Meaning | Drill-down | +|-------|---------|-----------| +| `markets_in_dispute_window` | resolved markets (`status 3`, `payout 1`) still disputable, no dispute row yet | `list_markets_in_dispute_window(oracle, from, limit)` | +| `disputes_awaiting_response` | open disputes the oracle has not answered | `list_oracle_disputes` → `stage = awaiting_response` | +| `disputes_awaiting_decision` | open disputes answered, now with the resolver/committee | `list_oracle_disputes` → `stage = awaiting_decision` | + +**Computed on read** (time-dependent, cheaper to derive than to store): + +| Field | Meaning | +|-------|---------| +| `markets_awaiting_resolution` | this oracle's `status 1` markets past betting close (a `by_oracle_status` walk) | +| `oldest_unresolved_age` | seconds since betting close of the oldest such market | +| `resolution_time_p50` / `resolution_time_p95` | latency percentiles read off the 8-bucket `resolution_time_hist` | + +**Latency accounting** at resolve (`markets_resolved` just incremented): +`resolved_late_count += (now > result_expiration)`; `avg_resolution_time` running-mean of +`rt = now − betting_expiration`; `resolution_time_hist[bucket(rt)] += 1` over 8 buckets +(≤1h, ≤6h, ≤24h, ≤3d, ≤7d, ≤14d, ≤30d, >30d). Open-ended markets contribute `rt = 0`. + +**Dispute-lifecycle audit (P1):** opening a dispute emits the `pm_dispute_opened` virtual op, and +`pm_dispute_finalize` / `pm_dispute_auto_close` carry the `oracle` field — so an oracle's dispute +history (and the disputer's) is reconstructable from `account_history` alone. + +### New Oracle Detection + +`markets_resolved < 20` is treated as an unproven oracle: the confidence shrink above pulls the score +toward the 6000 prior (UI may badge it as new). All scores and the workload/latency fields are computed +on read; only the raw counters and stored gauges live on `pm_oracle_object`. + +--- + +## 15. Position Transfers + +### Operation + +``` +pm_transfer_position { bet_id, to_user, amount, memo } +``` + +- Transfer all or part of a bet's tokens to another account +- Transferred tokens retain original market and outcome +- Payout goes to current holder at resolution +- No slippage, no market impact — pure record reassignment +- Works for both Onix Binary and Onix Multi positions + +### Memo Privacy Model + +| Mode | Format | Visibility | +|------|--------|------------| +| Plaintext | String not starting with `#` | Public on-chain | +| Encrypted | String starting with `#` | Private — only sender and recipient can decrypt | + +Encryption: ECIES shared-secret `ECDH(sender_memo_private, recipient_memo_public)` using VIZ account memo keys (standard Graphene model). Client-side encryption/decryption. + +--- + +## 16. Lazy Liquidity Pool + +### Parameters + +| Median-voted parameter | Role | +|---------|-------------| +| `pm_lazy_pool_enabled` | pool kill-switch | +| `pm_lazy_alloc_percent` | share of free balance allocated per market (bp) | +| `pm_lazy_max_total_alloc_percent` | cap on the pool fraction across active markets (bp) | +| `pm_lazy_recall_step_percent` | graduated-recall step on idle markets (bp) | +| `pm_lazy_lock_sec` | deposit lock period (seconds) | +| `pm_lazy_emergency_penalty_percent` | penalty on locked profit for emergency withdrawal (bp) | +| `pm_lazy_min_liquidity_fee_percent` | minimum market `liquidity_fee_percent` (bp) for the pool to allocate; below it the market gets no pool liquidity (reward floor) | +| `pm_min_liquidity` | minimum allocation per market (also the market seed floor) | + +### Deposit + +- First depositor: `shares = amount` +- Subsequent: `new_shares = amount × total_shares / pool_equity`, where (audit #4) + `pool_equity = free_balance + allocated_balance + leverage_fund_used − pending_withdrawals` + — the governance NAV. Pricing off `free_balance` alone (or omitting `leverage_fund_used`) + understates equity by the lent principal while leverage loans are outstanding (that principal + is solvency-checked to return ≥ loan on close), letting a depositor time entry against open + loans and over-mint reward weight, skimming yield from honest LPs. Falls back to `1:1` + (`shares = amount`) when `pool_equity ≤ 0`. +- Lock timer: `unlock_time = now + pm_lazy_lock_sec` +- Reward settlement before share calculation: `pending += shares × (pool.rps − user.snapshot) / PRECISION` + +### Auto-Allocation + +On market activation (status → 1): + +``` +alloc_amount = free_balance × allocation_percent / 100 +× (1 − active_market_penalty_pct / 100) ^ oracle_active_market_count +× (1 − fault_penalty_pct / 100) ^ oracle_active_fault_stamps +``` + +Reward-floor gate (checked first): if the market's `liquidity_fee_percent < +pm_lazy_min_liquidity_fee_percent`, the pool allocates **nothing** — it only subsidizes markets whose +LP fee pays it enough. The creator's own seed remains the sole liquidity. + +Checks: `alloc_amount ≥ min_market_allocation`, `allocated + alloc_amount ≤ total × max_total_allocation / 100`. + +Pool LP inserted with `user=0`. Participates identically in time-weighted fee distribution. + +### Reward Distribution (Lazy Accounting) + +On market resolution with pool LP profit: + +``` +profit = lp_return − allocation_amount +if profit > 0 AND total_shares > 0: + pool.reward_per_share += profit × PRECISION / total_shares +``` + +User reward (computed on read): + +``` +live_reward = pending_rewards + shares × (pool.rps − user.snapshot) / PRECISION +``` + +### Planned Withdrawal + +From consolidated unlocked record (full or partial): + +``` +1. Run unlock consolidation +2. Settle rewards: pending += shares × (rps − snapshot) / PRECISION +3. Share value = shares_to_burn × free_balance / total_shares +4. Reward portion = pending_rewards × withdraw_percent / 100 +5. Total payout = share value + reward portion +``` + +### Emergency Withdrawal + +All deposits (locked + unlocked): + +``` +1. Settle rewards +2. total_value = shares × free_balance / total_shares + pending_rewards +3. profit = total_value − principal_deposited +4. if profit > 0: penalty = profit × (locked_shares / total_shares) × emergency_penalty / 100 +5. Penalty → pool reward_per_share +6. User receives: total_value − penalty +``` + +### Opportunity-Cost Protection + +> **Governance vs. fixed.** Only the recall **step size** is median-voted — `pm_lazy_recall_step_percent` +> (how much is pulled per idle step). The rest is **hardcoded** and needs a hardfork to change: the +> **10-step** division (`window/10`, `check_step ≥ 10`), the idle test (a step is idle when *no new bets* +> arrived since the last check — `bets_sum ≤ bets_sum_at_check`), and the **5% per-active-market penalty** +> (`alloc × 95/100`). The fault-stamp penalty and its expiry window are likewise hardcoded. + +**A. Graduated Recall:** Market duration divided into **10 fixed steps**. At each step, if **no new bets** arrived since the last check, recall `pm_lazy_recall_step_percent` (bp, governance) of the current allocation back to the pool. + +**B. Active Market Penalty:** `factor = (1 − 5% ) ^ active_market_count` — a hardcoded recursive 5% reduction per concurrent active market from the same oracle. + +**C. Fault Stamps:** On bad market outcomes (no-contest, missed deadline, zero volume, dispute loss, no response, auto-close), the oracle gets a fault stamp that auto-expires after a fixed clean-operation window; each active stamp further reduces its allocation. (Penalty size + expiry are hardcoded.) + +--- + +## 16a. Opt-In Leverage (Lazy-Pool-Funded) + +Live since HF14; opt-in, governed by median kill-switch `pm_leverage_enabled` (default off). + +- **Open** (`pm_leverage_open`) — a bettor posts collateral; the Lazy Pool **loans** the margin from + `free_balance` (capped by `leverage_fund_used`; checked against `pm_leverage_fund_percent`, + `…_max_per_position_bp`, `…_max_position_ratio_percent`, `…_min_market_liquidity` at open time). No + token emission — the position is fully collateralized from the system's view. A leveraged open does + **not** create a `pm_bet`; the curve weight is held on the `pm_leverage_position_object`. +- **Liquidation** — runs against **pre-bet reserves** so the pool recovers `min(cancel_value, + obligation) ≥ loan`: opposing-bet cascade (`pm_place_bet`) and settlement force-close are always + full-recovery (loan + interest → pool); the **only** bounded bad-debt path is a same-side + `pm_cancel_bet` (Case B). The cascade is **not** gated by `pm_leverage_enabled` (the flag blocks only + new opens), so disabling leverage never strips protection from open positions. +- **Exit residual → `forfeit_pool`** — on close/liquidate the holder is paid the floored + `cancel_value`; the sub-mVIZ remainder `curve_residual = total_bet − cancel_value` is **dust of the + pool's own curve assets**, so it is routed to the market's `forfeit_pool` (accrues to the remaining + bettors at settlement), not the DAO fund. This keeps `pool + bettor + residual = total_bet` exact on + every exit (fix `6b002c14`; the general final-payout dust rule above still sends to the DAO fund). +- **Close timing (does NOT wait for the oracle)** — a leveraged position is a bet on the market + *price* (crowd sentiment), settled at its `cancel_value`; it is independent of the resolved + outcome. So `process_pm_markets` **force-closes it the moment new betting is impossible**: at + `betting_expiration` for fixed-deadline markets (i.e. *before* the oracle resolves), or at + resolve/void (`status >= 3`) for open-ended markets (`betting_expiration == 0`). The holder never + bleeds funding, and can never be funding-liquidated, during the post-betting resolve + dispute + window. `settle_market` / `return_liquidity` still force-close as an idempotent backstop. +- **Virtual ops** — `pm_leverage_resolve` (force-close at settlement, with outcome + leverage), + `pm_leverage_liquidate` (mid-market, `reason` 0 opposing / 1 cancel). +- **Governance weight** — Lazy-Pool depositors keep their PM-dispute and DAO-committee vote weight + (pool NAV → vesting-shares via `get_vesting_share_price`, HF14-gated). + +API: `get_account_leverage_positions`, `get_market_leverage_positions`, `get_lazy_pool`. + +## 16b. Batch / Commit-Reveal Betting (anti-MEV) + +Live since HF14 for **binary** markets (multi forces `allow_instant_bet` until LMSR batch lands); +opt-in per market (`allow_batch` / `allow_instant_bet`), median kill-switch `pm_commit_reveal_enabled`. + +- `pm_place_bet` with `mode = 1` queues a **batch** bet; `pm_commit_bet` (commitment hash + escrow) → + `pm_reveal_bet` runs the **commit-reveal** flow. Unrevealed commitments forfeit + `pm_commit_no_reveal_penalty_percent` (bp) via `pm_commit_forfeit`. +- At each epoch boundary (`pm_batch_epoch_blocks`, reveal window `pm_reveal_window_blocks`) queued bets + settle at a **uniform price** via the `pm_batch_settle` cron — only the net residual moves the AMM, so + intra-batch ordering carries no advantage and the `Σ reserve ≥ L` invariant is preserved. + +## 17. On-Chain Object Model + +All state lives in **chainbase objects** registered as core indexes at HF14 — there is no SQL database. Field-level definitions live in the operation/object headers and +are queryable read-only via the [`prediction_market_api` plugin](../plugins/prediction-market-api). The +reputation counters the prototype kept on a `users` table are now fields on `pm_oracle_object`. + +| Object (index) | Holds | Looked up by | +|---|---|---| +| `pm_oracle_object` | oracle registration, insurance, the 14 reputation counters, fault stamps, ban (`banned_until` + `banned_by`) | owner | +| `pm_market_object` | market config, CPMM reserves (`reserve_a/b`, `k`), `*_fee_percent` (bp), `status` / `payout_status`, timers, `dispute_mode`, `a_bets_sum` / `b_bets_sum`, oracle resolution statement (`decision_url` / `decision_reason`) | id / creator / oracle / result_expiration | +| `pm_outcome_object` | per-outcome LMSR `q`, `bets_sum`, `bets_count` (multi markets) | market + outcome | +| `pm_bet_object` | a bet — account, `side` / `outcome_index`, `amount`, curve `weight`, `time_penalty`, `status`, `mode` | market / account | +| `pm_liquidity_object` | an LP position — principal, deposit time, time-weight; `provider` empty ⇒ lazy-pool LP | market | +| `pm_commit_object` | a commit-reveal commitment hash + escrow (batch / commit-reveal) | market / account | +| `pm_dispute_object` | a dispute — disputer, `proposed_outcome`, fee escrow, timers, `status`, `dispute_mode`, oracle rebuttal (`oracle_response` / `oracle_response_time`) | market | +| `pm_dispute_vote_object` | one committee ballot — voter, `vote_outcome`, `vote_percent` (revisable until close) | market + voter | +| `pm_lazy_pool_object` | the singleton pool — `free_balance` / `allocated_balance` / `earned_balance`, `reward_per_share`, `leverage_fund_used`, `total_shares` | singleton (id 0) | +| `pm_lazy_deposit_object` | a depositor — shares, reward snapshot, unlock time | account | +| `pm_lazy_allocation_object` | the pool's silent LP allocation to one market + graduated-recall state (`bets_sum_at_check`, `check_step`, `recalled_amount`) | market | +| `pm_leverage_position_object` | an open leveraged position — collateral, loan, obligation, curve weight, `status` | account / market + status | +| `pm_creator_ban_object` | a banned creator — `banned_until`, `ban_count`, `banned_by` | ban account | + +Reputation metrics are computed on read (`compute_oracle_reliability_score()` — §14), not stored. All +percentage fields are basis points (`*_percent`, bp). Lazy-pool per-market allocations and +graduated-recall state live on `pm_lazy_allocation_object`; oracle fault stamps and reputation counters +live on `pm_oracle_object`. + +**Plugin-only (non-consensus):** `pm_market_meta_object` — off-chain-parsed market metadata +(category / tags / banned jurisdictions) for discovery and jurisdiction filtering; it is built by the +[`prediction_market_api`](../plugins/prediction-market-api) plugin from each market's opaque `metadata` +string and never participates in consensus. + +See [Prediction Market operations](../protocol/operations/prediction-markets) for the full field +definitions of these objects. diff --git a/docs/prediction-markets/whitepaper.md b/docs/prediction-markets/whitepaper.md new file mode 100644 index 0000000000..bf54060575 --- /dev/null +++ b/docs/prediction-markets/whitepaper.md @@ -0,0 +1,681 @@ +# Onix Protocol: LP-Guaranteed Prediction Markets on VIZ DLT + +**An Industry Whitepaper** + +*Anatoly Piskunov (On1x)* +*Version 2.0 — June 2026 (on-chain / HF14)* + +*Published (peer-facing) version — cite this for academic use:* +*Piskunov, Anatoly. "Combining Parimutuel Settlement with Automated Market Makers for Liquidity-Guaranteed Prediction Markets: The Onix Protocol" (June 22, 2026). SSRN: [ssrn.com/abstract=6978138](https://ssrn.com/abstract=6978138) · DOI: [10.2139/ssrn.6978138](https://dx.doi.org/10.2139/ssrn.6978138). Posted 16 Jul 2026, 29 pp. © All rights reserved; no reuse without permission.* + +--- + +> **On-chain status (HF14).** This paper was first written against the centralized prototype. The +> protocol now runs as **first-class consensus operations (`pm_*`) on VIZ DLT**, verified in +> `consensus_sim`. Live since HF14: both market types (CPMM binary + LMSR multi), the parimutuel +> zero-sum settlement, the **Lazy Pool**, an opt-in **leverage** subsystem, opt-in **batch / +> commit-reveal betting** (binary), bonded oracles, and a two-mode dispute system (committee / +> account). All percentage parameters are **basis points (bp): 10000 = 100.00%** (the prototype used +> permille). Sections below are annotated where the live design differs from the original prototype text. + +## Abstract + +Prediction markets aggregate dispersed information into prices, producing probability estimates that consistently outperform polls, expert panels, and statistical models. Yet adoption remains constrained by a single structural problem: **liquidity providers lose money.** + +Uniswap v3 LPs suffer impermanent loss. LMSR market makers risk their entire subsidy. CLOB market makers face adverse selection. Every existing model asks capital providers to accept downside risk in exchange for uncertain yield — and the data shows most of them lose. + +The **Onix Protocol** eliminates LP risk entirely. It is a prediction market architecture where LP principal is **structurally guaranteed** — not by insurance, not by hedging, but by the payout mechanics themselves. Winners are paid exclusively from losers' forfeited stakes. LP capital provides market depth but is never used to settle bets. + +This paper describes the Onix Protocol's two market types — **Onix Binary** (Constant Product Market Maker) and **Onix Multi** (LMSR pricing with parimutuel settlement) — along with the market architecture, oracle and dispute resolution system, lazy liquidity pool, opt-in leverage, governance model, and its implementation on VIZ DLT as consensus-level operations. + +--- + +## 1. The Problem: LP Risk in Prediction Markets + +Every prediction market needs liquidity. Without it, prices are meaningless — a bet that moves the market by 20% reveals the bettor's capital, not the crowd's wisdom. The fundamental question is: **who provides that liquidity, and what do they risk?** + +### 1.1 The Current Landscape + +| Platform | LP Model | LP Risk | Yield Source | +|----------|----------|---------|-------------| +| **Uniswap v3** | Concentrated AMM | Impermanent loss (often >5% annualized; >50% of v3 LPs underperform buy-and-hold) | Trading fees | +| **Aave / Compound** | Lending pool | Smart contract risk, liquidation cascades | Borrower interest | +| **Curve** | Stableswap AMM | Low IL for pegged assets, smart contract risk | Fees + CRV emissions | +| **Standard LMSR** | Market maker subsidy | Loss up to `b × ln(N)` — the entire subsidy | Bid-ask spread | +| **Polymarket (CLOB)** | Active market making | Inventory risk, adverse selection | Bid-ask spread | +| **Kalshi** | No LP concept | N/A (exchange model) | N/A | + +The pattern is clear: providing liquidity to prediction markets requires either active management skill (CLOB), tolerance for capital loss (LMSR), or acceptance of impermanent loss (AMM). None of these are suitable for retail participants. + +### 1.2 Why This Matters + +Prediction markets work best when they are deep and liquid. Deep markets produce accurate prices, attract informed traders, and generate the information value that makes prediction markets useful as a public good. But depth requires capital, and capital requires compensation for risk. + +The result is a chicken-and-egg problem: +- Thin markets → high slippage → poor UX → few bettors → low fees → no LP incentive → thin markets + +Breaking this cycle requires removing the risk from the LP side of the equation. If providing liquidity is risk-free, the barrier to entry drops to zero, and the flywheel can start spinning. + +--- + +## 2. The Onix Protocol + +### 2.1 Design Principles + +The Onix Protocol is built on three architectural invariants: + +1. **LP principal is structurally safe.** This is not a risk-mitigation strategy — it is a property of the payout architecture. LP capital and bet settlement draw from physically separate pools. + +2. **Losers fund winners.** All payouts (winner profits, oracle fees, creator fees, LP fees) are sourced exclusively from losers' forfeited stakes. Fees are computed at resolution as `floor(losers_sum × fee_bp / 10000)` (bp: 10000 = 100.00%), never deducted at bet time. + +3. **Dual market types, single guarantee.** Binary markets (Onix Binary) and multi-outcome markets (Onix Multi) use different pricing formulas but share the same settlement model and the same LP guarantee. + +### 2.2 Onix Binary (CPMM + Parimutuel Settlement) + +Onix Binary uses the Constant Product Market Maker formula — the same `x * y = k` invariant used by Uniswap — as the **pricing engine** for binary outcomes, with **parimutuel settlement** (losers fund winners pro-rata by weight), the same settlement model as Onix Multi. + +**Mechanics:** + +A market maintains two reserves, `reserve_a` and `reserve_b`, with constant product `k`: + +``` +k = reserve_a × reserve_b +``` + +When a user bets `amount` on outcome A (in the implementation, side 0 → `reserve_a`), the stake enters +that side's reserve and tokens are drawn from the **opposing** reserve: + +``` +new_reserve_a = reserve_a + amount +new_reserve_b = floor(k / new_reserve_a) +tokens_received = reserve_b − new_reserve_b +``` + +The `tokens_received` (called `weight`) is the user's **relative claim** on the winners' pool if outcome A wins (settlement is parimutuel — see below, identical to Onix Multi). Implied probability rises for the side that is bet (more money on A → `reserve_a` grows → `P(A)` grows): + +``` +P(A) = reserve_a / (reserve_a + reserve_b) +P(B) = reserve_b / (reserve_a + reserve_b) +``` + +**Settlement and proof of LP safety (parimutuel):** + +At resolution, winners receive their stake back plus a proportional share of the losers' pool, by weight (identical to Onix Multi): + +``` +winners_pool = losers_sum − fees +payout = bet_amount + (weight / total_winning_weight) × winners_pool − time_penalty_on_profit +``` + +LP principal `L` is returned unconditionally, and the guarantee is exact: + +``` +Money OUT = L + winning_bets + winners_pool + fees = L + winning_bets + losing_bets = L + all_bets = Money IN +``` + +Total payout is capped at `losers_sum` regardless of weights, so LP capital is never used to settle bets. The CPMM is the **pricing engine** (probability + weight); it does not gate payout. (The AM-GM relation `reserve_a + reserve_b ≥ 2√k = L` still holds for the pricing curve but is no longer relied upon for solvency.) + +**Worked example:** + +``` +Setup: 200 VIZ liquidity → reserve_a = 100, reserve_b = 100, k = 10,000 +Fees (bp): oracle 50 (0.5%), creator 50 (0.5%), liquidity 100 (1%) + +Alice bets 50 VIZ on A → receives weight 33.33 (price moves from 50% to 69%) +Bob bets 80 VIZ on B → receives weight 81.82 + +Resolution: A wins + Losers (Bob): 80 VIZ forfeited → losers_sum = 80 + oracle_fee = floor(80 × 50/10000) = 0.4 VIZ + creator_fee = floor(80 × 50/10000) = 0.4 VIZ + liq_fee = floor(80 × 100/10000) = 0.8 VIZ + winners_pool = 80 − 1.6 = 78.4 VIZ + + Alice (only winner, weight 33.33 of 33.33): + payout = 50 (stake) + 78.4 × (33.33/33.33) = 128.4 VIZ (minus any time penalty on profit) + LP return: 200 VIZ principal + share of 0.8 VIZ fee pool +``` + +### 2.3 Onix Multi (LMSR + Parimutuel Settlement) + +Onix Multi is the protocol's innovation for markets with 3–10 outcomes. It combines Hanson's Logarithmic Market Scoring Rule (LMSR, 2003) for real-time pricing with parimutuel settlement for LP safety. + +**Pricing (LMSR softmax):** + +For a market with outcomes {1, 2, ..., N}, each with quantity parameter `q_i`: + +``` +price(i) = exp(q_i / b) / Σ_j exp(q_j / b) +``` + +This is the softmax function — prices always sum to exactly 1.0 by construction. No arbitrage mechanism or split/merge operation is needed. + +The cost to buy Δ tokens on outcome i: + +``` +C(q) = b × ln(Σ_j exp(q_j / b)) + +cost = C(q + Δ·e_i) − C(q) +``` + +The parameter `b` controls price sensitivity (higher b = less price impact per bet). It is funded by the LP subsidy: `b = S / ln(N)` where S is the total subsidy. + +**The innovation — parimutuel settlement:** + +In **standard LMSR**, the market maker is the counterparty to all bets. If the crowd correctly predicts the outcome, the market maker loses up to `b × ln(N)` — potentially the entire subsidy. This is why LMSR has seen limited adoption outside corporate prediction markets (Microsoft, Inkling) where the operator absorbs the loss. + +**Onix Multi changes the payout source.** At resolution: + +``` +1. Oracle declares the winning outcome +2. Losers forfeit 100% → losers_sum +3. Fees deducted from losers_sum (bp; 10000 = 100.00%): + oracle_fee = floor(losers_sum × oracle_fee_bp / 10000) + creator_fee = floor(losers_sum × creator_fee_bp / 10000) + liq_fee = floor(losers_sum × liquidity_fee_bp / 10000) + winners_pool = losers_sum − fees +4. Winners receive: + payout = bet_amount + (tokens / total_winning_tokens × winners_pool) − time_penalty +5. LP subsidy returned unconditionally +``` + +Winners are paid by losers, not by the LP. The subsidy is architecturally separate from the settlement flow. + +**Proof of LP principal guarantee:** + +1. The LP deposits `S` VIZ as subsidy, which funds market depth. +2. During betting, users pay VIZ → receive outcome tokens. The VIZ accumulates as the betting pool. +3. At resolution, losers' forfeited stakes fund winner payouts and fees. The subsidy `S` was never in the payout pool. +4. The subsidy is returned to the LP unconditionally, regardless of outcome. + +**Comparison:** + +| Dimension | Standard LMSR | Onix Multi | +|-----------|---------------|------------| +| LP role | Counterparty to all bets | Depth deposit (not counterparty) | +| LP max loss | `b × ln(N)` (entire subsidy) | **Zero** | +| Winner payout | 1 token = 1 unit of currency | Token = proportional claim on losers' pool | +| CTF split/merge needed? | Yes (enforce price sum = 1) | No (softmax guarantees it) | + +**Worked example (3-outcome election):** + +``` +Setup: b = 1000, outcomes = [A, B, C], subsidy = 1000 VIZ +Initial: price(A) = price(B) = price(C) = 33.3% + +Alice bets 50 VIZ on A → ~47 tokens (price: 33% → ~38%) +Bob bets 100 VIZ on B → ~88 tokens +Carol bets 30 VIZ on C → ~29 tokens + +Resolution: A wins + Losers: Bob (100) + Carol (30) = 130 VIZ + Fees (200 bp = 2% total): 2.6 VIZ + winners_pool = 127.4 VIZ + + Alice: 50 + (47/47 × 127.4) = 177.4 VIZ + LP: 1000 VIZ returned in full + share of liquidity fees +``` + +### 2.4 Edge Cases + +| Scenario | Outcome | +|----------|---------| +| All bets on the winner | `losers_sum = 0` → every bettor gets back exactly their bet amount. LP subsidy returned. Zero-sum. | +| No bets on the winner | Entire losers' pool is undistributed → LP bonus. LP profits maximally. | +| Zero-volume market | LP subsidy returned in full. No fees, no payouts. | +| Single bettor wins | That bettor receives `bet_amount + winners_pool`. LP subsidy returned. | + +--- + +## 3. Market Architecture + +### 3.1 Market Lifecycle + +```mermaid +stateDiagram-v2 + direction LR + state "Waiting (0)" as Waiting + state "Active (1)" as Active + state "Closed (2)" as Closed + state "Resolved (3)" as Resolved + state "Deleted (-1)" as Deleted + state "Paid out" as Paid + [*] --> Waiting + Waiting --> Active: oracle accepts + Waiting --> Deleted: oracle rejects + Active --> Closed: betting expires + Active --> Resolved: early resolution (if allowed) + Closed --> Resolved: oracle resolves + Resolved --> Paid: grace period (12h) + Deleted --> [*] + Paid --> [*] +``` + +Markets are created by a market creator, reviewed and accepted by an oracle (who stakes insurance), open for betting, resolved with an outcome, and paid out after a dispute grace period. + +### 3.2 Fee Model (Losers-Only Fee Extraction) + +A distinctive feature of the Onix Protocol is that **no fees are deducted at bet time**. The full bet amount enters the market reserves. Fees are computed only at resolution, exclusively from the losing side's forfeited stakes: + +```mermaid +flowchart TD + LS["losers_sum (100% of losing bets)"] + LS --> OF["oracle_fee = floor(losers_sum × oracle_fee_bp / 10000)"] + LS --> CF["creator_fee = floor(losers_sum × creator_fee_bp / 10000)"] + LS --> LF["liquidity_fee = floor(losers_sum × liquidity_fee_bp / 10000)"] + LS --> WP["winners_pool = losers_sum − all fees"] +``` + +This provides a structural guarantee: fees and winner payouts draw from completely separate funding sources. Fee extraction can never compete with winner obligations. + +**Oracle fee terms are frozen at acceptance (offer→quote).** The creator publishes a *maximum* the oracle may charge (`oracle_fee_percent` ceiling in bp + `oracle_fixed_fee` ceiling); when the oracle accepts it quotes its actual terms (≤ the creator's ceiling and ≤ the median governance cap `pm_max_oracle_fee_percent`), which are frozen onto the market and a `pm_market_accepted` virtual op is emitted. A self-oracle freezes its terms at creation. The **oracle fixed fee** (per-market) is paid from the losers' pool remainder (never minted). A **market creation fee** goes to the DAO fund as anti-spam protection. + +### 3.3 Time-Weighted LP Distribution + +LP fee shares are distributed proportionally to `amount × max(1, seconds_to_expiration)`: + +``` +weight_i = amount_i × max(1, sec_to_expiration_i) +fee_share_i = floor(total_fee_pool × weight_i / Σ weight_j) +``` + +Early LPs earn dramatically more per unit of capital than late LPs. In a 48-hour market, an LP who deposits at hour 1 earns ~2,400x more per VIZ than one who deposits at hour 47. + +Each deposit is tracked as an independent position — multiple deposits by the same user are weighted and paid separately. LP principal is always returned in full, regardless of market outcome. + +### 3.4 Time Penalty for Late Bets + +To discourage last-minute betting (which carries less uncertainty risk), a configurable time penalty applies to bets placed near expiration: + +``` +if time_to_expiration < penalty_window: + ratio = 1 − (time_to_expiration / penalty_window) + penalty_ratio = ratio² // quadratic (default) + time_penalty = floor(penalty_ratio × max_penalty) +``` + +The penalty applies **only to profit**, never principal. A winning bettor always receives at least their original stake. The quadratic curve is gentle early in the penalty window and steep late, rewarding "somewhat late" over "extremely late." + +### 3.5 Position Transfers + +Positions are transferable between accounts via a native protocol operation: + +``` +pm_transfer_position { bet_id, to_user, amount, memo } +``` + +No slippage, no market impact — pure record reassignment. The `memo` field supports both plaintext and encrypted modes (ECIES via VIZ account memo keys), enabling P2P deals, OTC trading, and private annotations. + +This is the only composability feature from the Conditional Tokens Framework (Polymarket/Gnosis) that provides real user value. CTF split/merge is architecturally unnecessary — Onix pricing formulas guarantee price coherence by construction. + +--- + +## 4. Oracle and Dispute Resolution + +### 4.1 Bonded Oracle Model + +Oracles in the Onix Protocol are not trusted by default — they are **bonded**. Each oracle must: + +- Register with a one-time fee (default 10 VIZ) +- Deposit insurance (minimum 5,000 VIZ) +- Accept markets explicitly (staking their insurance on each acceptance) +- Resolve markets with an outcome and supporting evidence (decision URL) + +The insurance bond creates accountability: oracles who misresolve, miss deadlines, or lose disputes have their insurance slashed. The bond must exceed the oracle's potential manipulation profit for the economic security model to hold. + +Oracle revenue comes from two sources: +1. **Fixed fee** (per market) — compensates for staking insurance and providing resolution +2. **Percentage fee** (from losers' pool at resolution) — scales with market volume + +### 4.2 Dispute Arbitration + +Any bettor can challenge a resolution within a grace period by paying a dispute fee. During disputes, all payouts are frozen. + +Resolution runs in one of **two per-market modes**, chosen at creation: + +- **Committee mode (`dispute_mode = 0`, default)** — the *whole SHARES electorate* decides by + **stake-weighted vote** (`pm_dispute_vote`), tallied deterministically by the `pm_dispute_finalize` + cron at `voting_end_time`. It is an **open public hearing**: the live tally is queryable and votes are + **not** hidden behind commit-reveal (a deliberate, permanent choice — the DAO resolves disputes as + transparently as possible). Because new evidence surfaces during the hearing, **a ballot is revisable** + until close (a repeat vote overwrites the prior one). A voter's weight is its `effective_vesting_shares` + **plus its Lazy-Pool stake converted to vesting-shares**, so DAO members who park VIZ in the pool keep + their governance weight. +- **Account mode (`dispute_mode = 1`)** — a single named `dispute_resolver` (recommended multisig) + issues the verdict (`pm_dispute_resolve`). + +The verdict logic in either mode: + +**If the oracle was wrong (overturned):** +- The correct outcome is applied and payouts recalculated. +- The disputer gets their fee back **plus a reward carve-out** drawn from the slashed insurance, sized as `dispute_fee × pm_dispute_reward_multiplier` (bp; e.g. 30000 = ×3), capped by the slash. +- **The remainder of the slash is added to the winners' pool** (via `forfeit_pool`) — it goes to the winning bettors, **not** to a resolver or the DAO. Neither the committee voters nor the account-mode resolver receive any reward (committee voting is an unpaid governance duty). +- The oracle's insurance is slashed (scaled by consensus strength in committee mode, or by the resolver's `penalty_amount` in account mode), with optional ban. + +**If the oracle was right (upheld):** +- The disputer **forfeits the whole dispute fee to the oracle** (compensation for the bad-faith challenge). +- Original payouts proceed unchanged. + +**Denial-of-resolution prevention:** If the resolver fails to act within 14 days, disputes auto-close: all bets and LP are refunded, the oracle is penalized, and the disputer's fee is returned. This guarantees funds are never frozen indefinitely. + +### 4.3 Oracle Reputation Scoring + +The protocol tracks 14 on-chain metrics per oracle and computes a reliability score (0–100): + +``` +reliability_score = clamp(0, 100, + 50 (base) + − 0.40 × dispute_loss_rate × 100 + − 0.10 × excess_no_contest × 100 + − 0.20 × deadline_miss_rate × 100 + − 0.15 × (1 − dispute_response_rate) × 100 + + volume_bonus (0–25) + + experience_bonus × freshness_multiplier (0–25) + − 15 × bans_received +) +``` + +Key design choices: +- **Rates, not counts** — 1 dispute lost out of 100 (1%) scores better than 1 out of 2 (50%) +- **Neutral start at 50** — new oracles must earn reputation, not start at 100 +- **Freshness decay** — inactive oracles lose their experience bonus over time +- **Volume tiers** — high-volume oracles receive bonus points for proven track record + +The reliability score combines with a risk factor (insurance-to-bets ratio) to produce a **composite trust score** — the primary metric shown to users. + +### 4.4 No-Contest and 3-Outcome Resolution + +An oracle who cannot verify an outcome can voluntarily declare **no-contest**, triggering refunds at a reduced penalty (50% of dispute fee from insurance — much cheaper than losing a dispute). This creates an incentive gradient: + +| Scenario | Oracle Cost | Ban Risk | +|----------|------------|----------| +| Voluntary no-contest | 500 VIZ | None | +| Dispute loss | 1,000+ VIZ + extra penalty | Permanent or temporary | +| Missed deadline | 250 VIZ (auto-penalty) | None (but reputation damage) | + +If users believe the oracle abused no-contest, they can dispute it. The resolver then chooses from **three** possible correct outcomes: A wins, B wins, or confirm no-contest. This prevents oracles from using no-contest to avoid paying out winning bettors. + +--- + +## 5. Lazy Liquidity Pool + +### 5.1 The Capital Deployment Problem + +Individual LP provision requires active market selection. Most users won't manually evaluate and deposit into specific markets. The result: most markets launch with only the creator's initial liquidity, producing thin order books and high slippage. + +### 5.2 Automated Pool-to-Market Allocation + +The Lazy Liquidity Pool solves this by accepting deposits and **automatically allocating** a percentage of the pool's free balance to every new market when it activates: + +``` +alloc_amount = free_balance × allocation_percent / 100 +``` + +Allocations are computed from the current free balance (not the original total), creating geometric decay — the pool can never be fully depleted: + +``` +After 50 markets (2% allocation each): ~357 VIZ free from original 1,000 +After 100 markets: ~133 VIZ still free +``` + +A maximum total allocation cap (default 70%) provides additional safety. + +### 5.3 Reward Distribution (one shared accumulator) + +The problem: when a market resolves with pool profit, that profit must be split among **all** current +depositors in proportion to their shares — but iterating every depositor on every market would be O(N) +and unbounded. The pool avoids that with **one global running total**, `reward_per_share` ("rps"): + +``` +// When a market resolves with pool LP profit, the per-share value of the pool rises once: +pool.reward_per_share += profit × PRECISION / total_shares + +// A depositor's earnings = their shares × how much rps has risen since they last touched the pool: +live_reward = pending + shares × (pool.reward_per_share − user.snapshot) / PRECISION +``` + +In plain terms: every depositor "owns" a slice of each rise in `reward_per_share`, and their reward is +just `shares × (current rps − the rps recorded when they last deposited/withdrew)`. A depositor's own +record is touched **only when they act** (deposit or withdraw); until then their entitlement accrues +silently in the global number. So distributing profit to thousands of depositors is **O(1)** — a single +addition — and no funds are paid until claimed. This is the well-known accumulator pattern from +[SushiSwap's MasterChef contract](https://github.com/sushiswap/masterchef/blob/master/contracts/MasterChef.sol) +(and Compound's cToken index) — `PRECISION` (1e9) keeps the integer division exact. + +### 5.4 Opportunity-Cost Protection + +The pool auto-allocates to every market, creating an attack vector: a malicious oracle could create long-duration zero-volume markets to lock pool capital. Three mechanisms address this: + +**Graduated Early Recall:** The market's duration is divided into 10 steps. At each step, if betting volume is below a threshold (1% of allocation), 10% of the current allocation is recalled to the pool. A completely idle 30-day market loses ~60% of its allocation. + +**Active Market Penalty:** Each additional active market from the same oracle reduces that oracle's allocation by 5% (recursive). An oracle with 10 active markets receives ~60% of the base allocation per market, incentivizing quality over quantity. + +**Fault Penalty Stamps:** Bad outcomes (missed deadlines, disputes lost, zero-volume resolutions) generate penalty stamps that further reduce future allocations. Stamps auto-expire after 10 days of clean operation. + +### 5.5 Opt-In Leverage (Lazy-Pool-Funded) + +The Lazy Pool serves **two roles from one `free_balance`**: silent market-LP allocations *and* funding for +an **opt-in leverage** subsystem. A bettor can open a leveraged position (`pm_leverage_open`) where margin +is a **loan from the pool** — no token emission, the position stays fully collateralized from the system's +view. The binary "jump risk" that breaks CLOB liquidation engines is handled by **liquidating against +pre-bet reserves**: an opposing-bet or settlement force-close recovers `min(cancel_value, obligation) ≥ +loan`, so the pool gets its loan plus interest back; the only bounded bad-debt path is a same-side +`pm_cancel_bet`. A median kill-switch (`pm_leverage_enabled`, default off) blocks *new* opens, but the +protective liquidation cascade is deliberately **not** gated by it — toggling leverage off never strips +protection from open positions. The pool earns leverage interest in addition to LP yield, accounted via the same shared `reward_per_share` accumulator as in §5.3. Settlement of pool loans emits `pm_leverage_resolve` / `pm_leverage_liquidate`. + +A leveraged position is a bet on the market **price** (crowd sentiment), not on the oracle's verdict — +so it **does not wait for resolution**. It is force-closed at its `cancel_value` the instant new betting +becomes impossible: at `betting_expiration` for a fixed-deadline market (before the oracle even reports), +or at resolve for an open-ended one. The holder therefore never carries funding, nor faces liquidation, +across the resolution + dispute window. + +--- + +## 6. Governance + +### 6.1 Delegate-Voted Chain Parameters + +VIZ uses Delegated Proof of Stake (DPoS) consensus where elected delegates (validators) govern chain parameters through a median-vote mechanism: + +1. Each delegate publishes preferred values for all parameters +2. The network computes the **median** of all active delegates' votes +3. Parameters change automatically when the median shifts — no hard fork, no deployment + +All prediction-market parameters (fees, penalties, insurance requirements, dispute windows, lazy-pool +settings, leverage knobs, batch/commit-reveal timing) are delegate-voted. All percentage parameters are +in **basis points (bp), 10000 = 100.00%**: + +| Examples | Governance | +|----------|-----------| +| `pm_dispute_fee`, `pm_max_oracle_fee_percent` (bp) | Delegate median vote | +| `pm_dispute_grace_sec`, `pm_dispute_vote_period_sec` | Delegate median vote | +| `pm_dispute_approve_min_percent`, `pm_dispute_reward_multiplier` (bp) | Delegate median vote | +| `pm_lazy_*` allocation/recall, `pm_leverage_*` (enabled, fund %, max position) | Delegate median vote | +| `pm_commit_reveal_enabled`, `pm_batch_epoch_blocks`, `pm_reveal_window_blocks` | Delegate median vote | + +Hard forks are only needed for structural changes (new operation types, formula changes), not for economic tuning. Kill-switches (`pm_leverage_enabled`, `pm_commit_reveal_enabled`) let governance disable a whole subsystem by median vote without a fork. + +### 6.2 Jurisdictional Client Model + +VIZ DLT is infrastructure, not an operator — analogous to how Bitcoin is a ledger, not a money transmitter. The protocol is neutral and permissionless. Legal obligations attach to **client applications**, not to the consensus algorithm. + +Any jurisdiction can build a compliant client on VIZ DLT: + +| Client Component | Implementation | +|-----------------|---------------| +| Pre-approved oracles | Client whitelist of licensed, KYC-verified oracles | +| Pre-approved resolvers | Government-approved dispute resolution bodies | +| KYC/AML | Client-level identity verification | +| Fee routing as tax revenue | `dao_fund_account_id` → state treasury account | +| Market restrictions | Client filters by allowed categories | +| Betting limits | Client-enforced per-user caps | + +The same protocol operations (`pm_place_bet`, `pm_resolve`, `pm_dispute`) work identically for permissionless and regulated clients. The difference is entirely at the client layer. + +--- + +## 7. Competitive Landscape + +### 7.1 Platform Comparison + +| Dimension | Onix (Forecaster) | Polymarket | Kalshi | Standard LMSR | +|-----------|-------------------|------------|--------|---------------| +| **Pricing** | CPMM (binary) / LMSR softmax (multi) | CLOB | CLOB | LMSR | +| **LP Risk** | **Zero** (structural guarantee) | Inventory risk | N/A | Up to `b × ln(N)` | +| **LP Knowledge** | Low (deposit and earn) | High (manage orders) | N/A | Medium | +| **Fee Model** | % of losers' pool at resolution | Bid-ask spread | Exchange fees (1-7%) | Spread | +| **Oracle** | Per-market bonded + committee dispute | UMA Optimistic Oracle | Kalshi (CFTC-regulated) | Operator | +| **Late Bet Penalty** | Quadratic, configurable | None | None | None | +| **Position Transfer** | Native protocol operation + encrypted memo | CTF (ERC-1155) | None | None | +| **Governance** | Delegate-voted parameters | Team multisig | CFTC process | Operator | +| **Infrastructure** | VIZ DLT (consensus-level) | Polygon (smart contracts) | Proprietary servers | Various | + +### 7.2 Why CTF Split/Merge Is Unnecessary + +Polymarket uses the Gnosis Conditional Tokens Framework (CTF) where positions are ERC-1155 tokens that can be split and merged to enforce price coherence (prices sum to $1). + +Under the Onix Protocol, this mechanism is architecturally unnecessary: + +- **Onix Binary (CPMM):** `price(A) + price(B) = reserve_b/(reserve_a+reserve_b) + reserve_a/(reserve_a+reserve_b) = 1` — by definition +- **Onix Multi (LMSR softmax):** `Σ price(i) = Σ exp(q_i/b) / Σ exp(q_j/b) = 1` — by definition of softmax + +No arbitrage mechanism needed. Price coherence is a mathematical property of the formulas, not an external enforcement layer. + +### 7.3 The Flywheel + +``` +Risk-free LP → lower barrier for retail LPs + → more liquidity deposited + → deeper markets, less slippage + → better UX for bettors + → more volume + → more fees for LPs + → attracts even more LPs +``` + +"Passive yield without impermanent loss" is the value proposition that Uniswap, Balancer, and Curve cannot offer. For the crypto-native audience, this is a compelling narrative: earn yield by providing liquidity to prediction markets, with zero risk to principal. + +--- + +## 8. VIZ DLT: From Prototype to Protocol + +### 8.1 Current State + +The protocol began as a Telegram WebApp with a centralized backend (all market logic server-side) — a working prototype with known limits: no sybil resistance beyond Telegram accounts, no censorship resistance, no composability. **That migration is now done:** the full market logic runs **on VIZ DLT as consensus-validated `pm_*` operations** (HF14), exercised end-to-end in `consensus_sim`. The remainder of this section describes that on-chain architecture, now realized. + +### 8.2 Migration Architecture + +VIZ DLT is a Distributed Ledger Technology with ~3-second block times, DPoS consensus, named accounts (Graphene-style), and no general-purpose smart contracts. Prediction market operations will be implemented as **first-class consensus-validated operations** — not smart contracts, not `custom_json` payloads. + +| Layer | Examples | Consensus-Validated? | +|-------|---------|---------------------| +| **Protocol operations** | `pm_create_market`, `pm_oracle_accept_market`, `pm_place_bet`, `pm_commit_bet`/`pm_reveal_bet`, `pm_resolve_market`, `pm_dispute_create`/`pm_dispute_vote`/`pm_dispute_resolve`, `pm_lazy_deposit`/`pm_lazy_withdraw`, `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert` | Yes — every node validates | +| **Virtual operations** | `pm_payout` (per bettor), `pm_auto_payout`, `pm_market_accepted`, `pm_dispute_finalize`, `pm_dispute_auto_close`, `pm_oracle_missed_penalty`, `pm_lazy_recall`, `pm_batch_settle`, `pm_commit_forfeit`, `pm_leverage_resolve`/`pm_leverage_liquidate` | Yes — deterministic, generated at block time | +| **metadata / custom_json** | Dispute comments, market descriptions, UI metadata | No — display/indexing only | + +Every financial action (placing bets, adding liquidity, resolving markets, seizing insurance) is validated by every validator. Invalid operations are rejected before block inclusion. No Solidity, no gas estimation, no bytecode deployment. + +The frontend is a **fully headless web client** — no backend server, no database, no sessions. Private keys stored in the browser (encrypted), transactions signed locally and broadcast to public VIZ nodes. No Telegram dependency; the core app is platform-independent. + +### 8.3 Delivered Since the Prototype, and Remaining Roadmap + +**Delivered on-chain (HF14):** + +| Feature | Status | +|---------|--------| +| Commit-reveal + batch betting (binary, opt-in, median kill-switch) | ✅ Live | +| Opt-in leverage (Lazy-Pool-funded, pre-bet-reserve liquidation) | ✅ Live | +| Lazy Pool (auto-allocation, graduated recall, MasterChef accounting) | ✅ Live | +| Per-bettor / leverage settlement virtual ops + plugin API | ✅ Live | +| Lazy-pool stake as governance weight (PM disputes + DAO requests) | ✅ Live | + +**Remaining roadmap:** + +| Priority | Feature | Impact | +|----------|---------|--------| +| High | Shared liquidity pools (category-level AMMs) | Solves liquidity fragmentation at the architecture level | +| High | Automated data oracles (exogenous feeds) | Eliminates manipulation for objective markets | +| Medium | Tiered dispute windows (small vs large markets) | Better UX calibration | +| Medium | LMSR batch settlement (extend batch/commit-reveal to multi) | Multi markets currently force instant betting | +| — | Commit-reveal **dispute** voting | **Deliberately rejected** — disputes stay public hearings (see §4.2) | + +--- + +## 9. What Onix Does NOT Claim + +Honest disclosure of tradeoffs and limitations: + +- **LP profit is not guaranteed.** If a market has zero losing bets, there are no fees to distribute. LP gets principal back but earns nothing. + +- **Platform risk exists.** Bugs, exploits, and governance attacks are separate from the market maker model. The LP guarantee is structural (payout architecture), not insured (no external guarantee fund). + +- **Onix Multi tokens are not fixed-value instruments.** In standard LMSR, 1 winning token = 1 unit of currency. In Onix Multi, tokens are proportional claims on the losers' pool. If all bettors pick the winner, everyone breaks even. + +- **LP yield depends on volume, not depth.** A market with 100,000 VIZ subsidy and one with 1,000 VIZ subsidy earn the same absolute fee if both have identical betting volume and fee rates. The subsidy provides depth, not yield. + +- **DPoS governance has known tradeoffs.** Fewer validators than PoW/PoS, delegate concentration risks, token-weighted voting. These are inherent to the DPoS model (shared by EOS, Hive, Tron), not VIZ-specific. + +- **VIZ token liquidity is currently low.** Economic guarantees (insurance bonds, dispute fees) scale with token price. The protocol assumes that utility drives demand over time — the same bet every protocol-native token project makes. + +--- + +## 10. Conclusion + +The Onix Protocol addresses the fundamental barrier to prediction market adoption: LP risk. By structurally separating LP capital from bet settlement — in both binary (CPMM) and multi-outcome (LMSR + parimutuel) markets — Onix makes liquidity provision risk-free and accessible to retail participants. + +The key innovations: + +1. **LP principal guarantee** as an architectural invariant, not insurance +2. **Losers-fund-winners** settlement eliminating fee competition with winner payouts +3. **LMSR pricing with parimutuel settlement** (Onix Multi) — combining proven price discovery with LP safety +4. **Time-weighted LP distribution** rewarding early capital commitment +5. **Quadratic time penalty** on profit (never principal) for late bets +6. **Lazy Liquidity Pool** with automated allocation, graduated recall, and MasterChef accounting — also funding the opt-in **leverage** subsystem (pool-funded margin, pre-bet-reserve liquidation, never bad debt outside a bounded cancel-bet path) +7. **Bonded oracle model** with reputation scoring, offer→quote fee freezing, and a two-mode dispute system — committee disputes are **public hearings** with revisable, lazy-pool-weighted votes +8. **Opt-in anti-MEV** — batch / commit-reveal betting (binary) with a median kill-switch +9. **Consensus-level implementation** on VIZ DLT — no smart contracts, no gas, no external keepers; strictly **zero-sum** (the protocol never mints a token) + +The bet is straightforward: if zero-risk LP attracts capital, capital creates depth, depth improves prices, and prices attract bettors, then the Onix Protocol solves the prediction market liquidity problem. The protocol mechanics are mathematically verifiable. The economic hypothesis will be tested by the market. + +--- + +## 11. Author and Disclosure + +### Author + +**Anatoly Piskunov** (On1x) — Russian IT innovator, Web3/DLT developer, and creator of the VIZ blockchain. His work spans distributed ledger technology, decentralized social protocols, and economic models for digital communities. + +Key contributions include: VIZ Blockchain (Fair DPoS, social capital primitives), the Onix Protocol (LP-guaranteed prediction markets), Voice Protocol (censorship-resistant messaging), and extensive publications on blockchain economics and Web3 architecture. + +Full list of publications and projects: [https://on1x.com](https://on1x.com) + +### Disclosure + +The author of Forecaster and the Onix Protocol is also the creator of VIZ DLT. The migration roadmap proposes moving the platform to a blockchain the author designed and built. + +This is disclosed upfront. It is also the norm: Polymarket depends on Polygon Labs' infrastructure, Kalshi runs on its own servers, Augur designed the REP token it runs on. Every platform argues for its own infrastructure. The question is not whether the author has an interest — they always do — but whether the technical claims are falsifiable. Every formula, proof, and mechanism in this paper is mathematically verifiable and the codebase is open-source. + +--- + +## 12. References + +1. Hanson, R. (2003). *Combinatorial Information Market Design.* Information Systems Frontiers, 5(1), 107–119. — The Logarithmic Market Scoring Rule (LMSR). + +2. Adams, H., Zinsmeister, N., Robinson, D. (2020). *Uniswap v2 Core.* — Constant Product Market Maker (`x * y = k`). + +3. Adams, H., et al. (2021). *Uniswap v3 Core.* — Concentrated liquidity and impermanent loss analysis. + +4. Gnosis. *Conditional Tokens Framework (CTF) Documentation.* https://docs.gnosis.io/conditionaltokens/ — ERC-1155 prediction market positions. + +5. UMA Protocol. *Optimistic Oracle Documentation.* — Dispute escalation mechanism used by Polymarket. + +6. Leshner, R., Hayes, G. (2019). *Compound: The Money Market Protocol.* — cToken accumulator pattern (basis for reward_per_share). + +7. SushiSwap. *MasterChef Contract.* — Lazy accounting pattern for reward distribution. + +8. Piskunov, A. (2019). *VIZ blockchain system: technical description.* — VIZ DLT architecture, DPoS consensus, named accounts. + +9. Piskunov, A. (2019). *What is Fair DPoS.* — Governance innovation in delegated proof of stake. + +10. Piskunov, A. (2023). *VIZ as a Digital Representative Self-Governing State.* — Framework for blockchain systems as digital polities. diff --git a/docs/prediction-markets/workflows.md b/docs/prediction-markets/workflows.md new file mode 100644 index 0000000000..22c9d7076e --- /dev/null +++ b/docs/prediction-markets/workflows.md @@ -0,0 +1,754 @@ +--- +title: Prediction Markets — workflows & interaction diagrams +description: One canonical Onix binary market traced through every participant, with the zero-sum master ledger for normal and disputed resolution, and the verified operation / virtual-operation status. +--- + +# Workflows & interaction diagrams + +One **canonical scenario** traced through every participant. Each role sends specific **signed +operations**, is touched by specific **virtual operations**, and ends with a **tokens sent / received** +table for two outcomes: + +- **Normal resolve** — oracle resolves, grace passes, `pm_auto_payout` settles. No dispute. +- **Disputed resolve** — oracle resolves **A**, a dispute **overturns to B**, then settlement runs. + +All amounts are abstract **VIZ** units. All percents are **bp** (10000 = 100.00%). Settlement is strictly +**zero-sum** — no tokens are ever minted; `current_supply` is untouched: + +``` +Σ winner_payout + oracle_take + creator_take + lp_bonus + LP_principal + == Σ all bet amounts + LP_principal + forfeit_pool (+ insurance slash, in a dispute) +``` + +## Canonical market **M** (binary CPMM, A vs B) + +| Item | Value | +|------|-------| +| Engine | binary CPMM (`x·y=k`), `weight = tokens_out` | +| Seed liquidity (marketmaker) | **2000** → reserves A=1000 / B=1000 | +| `oracle_fee_percent` (oracle quote) | **1000** (10%) | +| `creator_fee_percent` | **500** (5%) | +| `liquidity_fee_percent` | **500** (5%) | +| `oracle_fixed_fee` (oracle quote) | **10** | +| `dispute_penalty_percent` | **+10000** (slash up to 100% of insurance ×consensus) | + +Illustrative chain props: `pm_market_creation_fee` 5, `pm_oracle_registration_fee` 10, +`pm_min_oracle_insurance` 5000, `pm_dispute_fee` 1000, `pm_dispute_reward_multiplier` 30000 (**3×**), +`pm_no_contest_penalty_percent` 5000 (50% of dispute fee), `pm_oracle_penalty_percent` 500 (5% of +insurance on a missed deadline), `pm_lazy_emergency_penalty_percent` 5000 (50% of profit), +`pm_leverage_pool_profit_percent` **R = 10%**, `pm_lazy_alloc_percent` 2000 (20%). + +### Roster + +| Actor | Role | Stake / action | +|-------|------|----------------| +| **maker** | creator + first LP | seeds 2000 liquidity | +| **orac** | external oracle | insurance 5000; quotes fee 10% + fixed 10 | +| **LP1** | in-market liquidity provider | adds 1000 liquidity | +| **A** | bettor — winner | 100 on **A**, early; weight 100 | +| **C** | bettor — late winner | 100 on **A** at T+85%; weight 100; time-penalty **50%** | +| **B** | bettor — loser | 200 on **B**; weight 200 | +| **D** | leverage **×10** winner | collateral 10 + loan 90 (market **L**) | +| **E** | leverage **×5** liquidated | collateral 20 + loan 80 (market **L**) | +| **LZ1** | lazy-pool depositor | deposits 1000 | +| **disp** | disputer | escrows dispute fee 1000 | + +> Curve weights (100 / 100 / 200) are written explicitly to keep the parimutuel arithmetic readable; +> a real CPMM hands out slightly less weight as reserves shift. + +## Interaction diagrams + +**Market lifecycle.** + +```mermaid +flowchart LR + W["Waiting (0)"] -->|oracle accepts| A["Active (1)"] + W -->|oracle rejects| X["Deleted (-1)"] + A -->|betting_expiration| C["Closed (2)"] + A -->|early resolution| R["Resolved (3)"] + C -->|oracle resolves| R + R -->|grace, no dispute| P["Paid out"] + R -->|dispute filed| D["Disputed"] + D -->|finalize / resolver| P +``` + +**Settlement — NORMAL resolve (A wins).** Losers fund winners; LP principal is untouched (zero-sum). + +```mermaid +flowchart TD + B["B loses 200 (losers_pool)"] --> POOL{"split 200"} + POOL -->|oracle_fee 20 + fixed 10| OR["oracle +30"] + POOL -->|creator_fee 10| CR["creator +10"] + POOL -->|liq_fee 10 + penalty 37| LPS["LPs +47"] + POOL -->|winners_pool 150 → profit 75| A["A → payout 175"] + POOL -->|profit 75 − time-penalty 37| C["C → payout 138"] + MK["maker + LP1 principal 3000"] -.returned in full.-> MK +``` + +**Dispute — oracle said A, overturned to B.** The punishment is the insurance slash (separate money). + +```mermaid +sequenceDiagram + participant O as Oracle + participant D as Disputer + participant V as Committee / Resolver + O->>O: resolve A + D->>V: pm_dispute_create (escrow dispute_fee) + O-->>V: mandatory response (deadline) + V->>V: pm_dispute_vote / pm_dispute_resolve → overturn to B + V-->>O: insurance slashed (5000) + V-->>D: fee back + reward (2000 from slash) + V->>V: settlement re-runs → B wins +``` + +## Master ledger — NORMAL resolve (A wins) + +`losers_sum = 200` (B). Fees off the losers' pool: +`oracle_fee = 200×10% = 20`, `creator_fee = 200×5% = 10`, `liq_fee = 200×5% = 10`, `oracle_fixed = 10`. +`winners_pool = 200 − 20 − 10 − 10 − 10 = 150`. `Σ winning weight = 200` (A 100 + C 100). + +- **A**: profit `150×100/200 = 75`, penalty 0 → **payout 175**. +- **C**: profit 75, time-penalty `75×50% = 37` (→ LP) → **payout 138**. +- **LP bonus** = `liq_fee 10 + penalties 37 = 47`, split by time in market: **maker ~31 / LP1 ~16**. +- **oracle_take** = `oracle_fee 20 + fixed 10 = 30`. **creator_take** = `creator_fee 10`. + +| Actor | sends | receives | net (this market) | +|-------|-------|----------|-------------------| +| maker | 2000 liquidity + 5 creation-fee | 2000 principal + 10 creator-fee + 31 LP-bonus | **+36** | +| orac | (10 reg-fee, 5000 insurance locked) | 30 oracle-take | **+30** | +| LP1 | 1000 liquidity | 1000 principal + 16 LP-bonus | **+16** | +| A | 100 | 175 | **+75** | +| C | 100 | 138 | **+38** | +| B | 200 | 0 | **−200** | + +**Zero-sum:** in `= bets 400 + LP principal 3000 = 3400`; out `= 175+138+0 + 30 + 10 + 47 + 3000 = 3400`. ✔ +The 5 creation-fee + 10 reg-fee leave to the **DAO fund** (not part of the market pool). + +## Master ledger — DISPUTED resolve (oracle said A → overturned to B) + +`disp` escrows `dispute_fee 1000`. The verdict overturns to **B**; the oracle is slashed. +With `dispute_penalty_percent = 10000` and consensus strength **100%**: `slash = 5000×100%×100% = 5000`. +Reward carve-out: `reward_target = fee×3 = 3000` → `bonus = 3000 − 1000 = 2000` (≤ slash). Disputer gets +`fee 1000 + bonus 2000 = 3000`. Remainder `slash − bonus = 3000 → forfeit_pool`. + +Now **B wins**. `losers_sum = 200` (A 100 + C 100). Fees 20/10/10 + fixed 10. +`winners_pool = 200 − 50 + forfeit 3000 = 3150`. `Σ winning weight = 200` (B). +- **B**: profit `3150×200/200 = 3150` → **payout 3350**. +- **oracle_take** still `30` (the *market* fee is paid from the frozen config even when overturned — the + punishment is the **insurance slash**, separate money). **creator_take** 10. **LP bonus** = liq 10. + +| Actor | sends | receives | net (this market) | +|-------|-------|----------|-------------------| +| maker | 2000 + 5 | 2000 principal + 10 creator-fee + ~6 LP-bonus | **+11** | +| orac | insurance −**5000** slashed | 30 oracle-take | **−4970** | +| LP1 | 1000 | 1000 principal + ~4 LP-bonus | **+4** | +| A | 100 | 0 | **−100** | +| C | 100 | 0 | **−100** | +| B | 200 | 3350 | **+3150** | +| disp | 1000 dispute-fee | 3000 (fee back + 2000 reward) | **+2000** | + +**Zero-sum:** in `= bets 400 + LP principal 3000 + dispute_fee 1000 + insurance slash 5000 = 9400`; +out `= B 3350 + oracle 30 + creator 10 + lp_bonus 10 + LP principal 3000 + disputer 3000 = 9400`. ✔ +The slash 5000 splits into disputer bonus 2000 + forfeit 3000 (→ B via the winners' pool). + +## Implementation status (verified against the code) + +**Regular operations — all 21 present** in the `operation` variant (`operations.hpp`), validated + +evaluated in `pm_evaluator.cpp`: +`pm_oracle_register`, `pm_oracle_update`, `pm_create_market`, `pm_oracle_accept_market`, `pm_place_bet`, +`pm_commit_bet`, `pm_reveal_bet`, `pm_cancel_bet`, `pm_add_liquidity`, `pm_withdraw_liquidity`, +`pm_resolve_market`, `pm_no_contest`, `pm_dispute_create`, `pm_dispute_vote`, `pm_dispute_resolve`, +`pm_transfer_position`, `pm_lazy_deposit`, `pm_lazy_withdraw`, `pm_leverage_open`, `pm_leverage_close`, +`pm_leverage_convert`. ✔ + +**Virtual operations** — emitted by `database::process_pm_markets()` / the evaluators: + +| Virtual op | Fires? | Trigger (code) | +|------------|--------|----------------| +| `pm_market_accepted` | ✔ | on `pm_oracle_accept_market` (accept) **and** self-oracle `pm_create_market` | +| `pm_payout` | ✔ | **per active bet** at settle — carries `account`, `market_id`, `bet_id`, `side`/`outcome_index`, `amount` (stake), `payout` (**0 on a loss**) | +| `pm_auto_payout` | ✔ | **once per market** at settle — a summary marker (`bets_sum`) alongside the per-bet `pm_payout`s | +| `pm_commit_forfeit` | ✔ | unrevealed commit past `reveal_deadline` | +| `pm_dispute_finalize` | ✔ | committee `voting_end_time` | +| `pm_dispute_auto_close` | ✔ | `auto_close_time` (anti-freeze) | +| `pm_oracle_missed_penalty` | ✔ | oracle missed `result_expiration` | +| `pm_lazy_recall` | ✔ | idle-allocation graduated recall step | +| `pm_batch_settle` | ✔ | epoch boundary | +| `pm_leverage_liquidate` | ✔ | mid-market liquidation: reason **0** opposing-bet, **1** cancel-bet (`cascade_liquidate`) | +| `pm_leverage_resolve` | ✔ | **settlement** of a leveraged position: carries `market_id`, `outcome_index`, `won` (solvent ⇒ positive), `pool_received`/`bettor_received`, and `leverage` (= `total_bet/collateral`) | + +> A leveraged position is force-closed at its `cancel_value` **as soon as new betting is impossible** — +> at `betting_expiration` (fixed-deadline markets, *before* the oracle resolves) or at resolve/void +> (open-ended markets). It is a price/sentiment bet, so it does **not** wait for the outcome: the pool +> takes `min(cv, obligation)`, the bettor gets the rest. **`pm_leverage_resolve`** marks that close +> (positive if `cv ≥ obligation`, else collateral lost); **`pm_leverage_liquidate`** is only for the +> *mid-market* opposing-bet / cancel-bet cascades. `settle_market` still force-closes as an idempotent backstop. + +See the [Plugin API](../plugins/prediction-market-api) for the read methods that surface each of these +(`get_account_leverage_positions`, `get_market_leverage_positions`, `get_creator_ban`, `get_dispute_votes`, …); +per-bettor results (`pm_payout`) and leverage settlements (`pm_leverage_resolve`) also appear in +`account_history`. + +## Roles in the canonical scenario + +Every participant traced through market **M** (and the leverage sub-market **L**): its interaction +diagram, the **signed** operations it sends, the **virtual** operations that touch it, a +**tokens sent / received** ledger for both outcomes, and a code-verification pointer. Each per-actor +ledger is a slice of the two [master ledgers](#master-ledger--normal-resolve-a-wins) above. + +### Market maker (creator + first LP) + +The maker creates market M, seeds the **2000** liquidity (becoming the first `pm_liquidity_object`), and +proposes the oracle's **offer ceiling**. It does **not** resolve (that's the oracle). + +```mermaid +flowchart LR + maker -->|pm_create_market| M[(pm_market_object
status=0)] + maker -->|seed 2000| LP0[(pm_liquidity_object
provider=maker)] + M -. fee 5 .-> DAO[(committee_fund)] + orac -->|pm_oracle_accept_market| M2[(M status=1)] + M2 -. VIRTUAL .-> VA[[pm_market_accepted]] + M2 ==>|pm_auto_payout| RET[principal 2000 + creator_fee + LP bonus] + RET --> maker +``` + +- **Sends:** `pm_create_market` (sets the `oracle_fee_percent`/`oracle_fixed_fee` **offer ceiling** plus + its own `creator_fee_percent` 5% and `liquidity_fee_percent` 5%; pays `pm_market_creation_fee` 5 → DAO, + locks `liquidity` 2000); optional `pm_add_liquidity` / `pm_withdraw_liquidity` (principal-safe, locked + from `betting_expiration` to resolution). +- **Touched by:** `pm_market_accepted` (oracle accepts, or self-oracle at creation); `pm_auto_payout` + (returns LP principal + time-weighted LP-bonus share). + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | 2000 liquidity + 5 creation-fee (→DAO) | 2000 principal + **creator_fee 10** + **LP-bonus ~31** | **+36** | +| disputed (→B) | 2000 + 5 | 2000 principal + creator_fee 10 + LP-bonus ~6 | **+11** | + +The creator fee is **still paid** from the frozen market config on an overturn — the dispute punishes the +**oracle** (insurance slash), not the maker. LP principal is returned unconditionally. + +- **Self-oracle variant:** `oracle == creator` → active at creation, `pm_market_accepted` fires with + `self_oracle=true`, and the maker also earns the `oracle_take`. +- **Verify:** `pm_create_market_evaluator`; LP via `settle_liquidity`; `committee_fund += pm_market_creation_fee`. + **Observe:** `get_market`, `list_markets_by_creator`, `get_market_liquidity` (`earned_fee`), `get_market_meta`. + +### Oracle (register → accept/quote → resolve) + +External oracle **orac** bonds insurance, **quotes** its fee at accept (≤ the maker's offer and ≤ +`pm_max_oracle_fee_percent`), and resolves. Its market fee is paid from the losers' pool; its bond is at +risk only on a missed deadline or a lost dispute. + +```mermaid +flowchart LR + orac -->|pm_oracle_register
insurance 5000| O[(pm_oracle_object)] + orac -. reg-fee 10 .-> DAO[(committee_fund)] + orac -->|pm_oracle_accept_market
quote fee 10% + fixed 10| M[(M status=1)] + M -. VIRTUAL .-> VA[[pm_market_accepted]] + orac -->|pm_resolve_market A| M3[(M status=3)] + M3 ==>|grace passes| VP[[pm_auto_payout]] + VP -->|oracle_take 30| orac +``` + +- **Sends:** `pm_oracle_register` (locks insurance 5000, pays reg-fee 10 → DAO, sets advisory list-price); + `pm_oracle_accept_market` (**quotes** fee 10% + fixed 10, each ≤ the creator's offer and ≤ the median + cap; freezes them onto M); `pm_resolve_market` (sets `winning_outcome`, opens the grace window); + optional `pm_oracle_update` / `pm_no_contest`. The standing list-price can also pre-authorise markets to + go live at creation via **auto-accept** — see the oracle ops doc. +- **Touched by:** `pm_market_accepted`; `pm_auto_payout` (credits `oracle_take`); + `pm_oracle_missed_penalty` (never resolves → slashes `pm_oracle_penalty_percent` of insurance → DAO, + refunds all bets). + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | insurance 5000 (locked) + reg-fee 10 (→DAO) | **oracle_take 30** = fee 20 + fixed 10 | **+30** | +| disputed (→B) | insurance −**5000 slashed** | oracle_take 30 | **−4970** | + +Even when overturned the oracle keeps the small **market fee** (frozen config); the punishment is the +**insurance slash**, split into the disputer's reward and the winners' `forfeit_pool`. Quoting **below** +the offer is allowed (price = reputation); quoting **above** is rejected. + +- **Verify:** `pm_oracle_register_evaluator`, `pm_oracle_accept_market_evaluator` (≤ offer, ≤ cap, freeze), + `process_pm_markets` missed-deadline scan. **Observe:** `get_oracle` (insurance, counters, reliability + score), `list_oracles`, `get_market` (frozen terms). + +### Oracle — upheld in a dispute (dispute winner) + +The oracle resolved **A**; a disputer challenged it but the verdict **upholds** A. The oracle keeps its +market fee **and** collects the forfeited `dispute_fee`; insurance untouched, `disputes_won++`. + +```mermaid +flowchart LR + orac -->|pm_resolve_market A| M[(market resolved A)] + disp -->|pm_dispute_create| D[(dispute)] + D ==>|uphold A| FIN[[pm_dispute_finalize / resolve]] + FIN -->|dispute_fee 1000| orac + FIN --> AUTO[[pm_auto_payout settles A]] + AUTO -->|oracle_take 30| orac +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| disputed, upheld | insurance 5000 (**not** slashed) | oracle_take 30 + **dispute_fee 1000** | **+1030** | +| normal (no dispute) | insurance 5000 (locked) | oracle_take 30 | **+30** | + +The challenge backfires and **pays the oracle**. A *good-faith* market (`dispute_penalty_percent < 0`) +can even hand the oracle a fee bonus when the outcome is changed — recognising an honest mistake. +**Verify:** uphold branch of `pm_dispute_finalize` / `pm_dispute_resolve`. **Observe:** `get_oracle` +(`disputes_won`, higher score), `get_dispute`. + +### Oracle — overturned + slashed (dispute loser) + +The oracle resolved **A**; the dispute **overturns to B** and insurance is **slashed**. It still collects +the tiny frozen market fee (fee and punishment are separate money) but loses a large slice of bond and +reputation. + +```mermaid +flowchart LR + orac -->|pm_resolve_market A| M[(resolved A)] + disp -->|pm_dispute_create proposed=B| D[(dispute)] + D ==>|overturn to B| FIN[[pm_dispute_finalize / resolve]] + FIN -->|slash 5000| INS[oracle.insurance ↓] + INS --> SPLIT[bonus 2000 → disputer
3000 → forfeit_pool → B] + FIN --> AUTO[[pm_auto_payout settles B]] + AUTO -->|oracle_take 30| orac +``` + +`slash = insurance 5000 × dispute_penalty_percent (100%) × consensus_strength (100%) = 5000`, +**redistributed** not burned: `bonus 2000 →` disputer, `3000 → forfeit_pool →` the new winners (B). Net +**−4970** vs **+30** undisputed. Slash scales with **consensus strength** (`winning_rshares / +max_rshares`); `dispute_penalty_percent < 0` (good-faith) → **no** slash. A slashed oracle is often +**banned** too (next role). **Verify:** overturn branch of `pm_dispute_finalize` / `pm_dispute_resolve`; +fee still from `mkt.oracle_fee_percent`. **Observe:** `get_oracle` (`total_insurance_slashed`, +`banned_until`), `get_dispute`. + +### Banned oracle (and banned creator) + +A ban is a **status**, not a transfer: `pm_oracle_object.banned_until` (and, for creators, a +`pm_creator_ban_object`) blocks the actor from **new** markets until the timestamp passes. It usually +rides along with an overturn slash, but moves no tokens by itself. + +```mermaid +flowchart LR + resolver -->|pm_dispute_resolve ban_oracle| O[(pm_oracle_object
banned_until = T)] + orac -->|pm_create_market / accept| CHK{now < banned_until?} + CHK -->|yes| REJ[REJECTED: 'Oracle is banned'] + CHK -->|no, expired| OK[allowed again] + resolver -->|ban_creator| CB[(pm_creator_ban_object)] + maker -->|pm_create_market| CHK2{banned?} + CHK2 -->|yes| REJ2[REJECTED: 'Creator is banned'] +``` + +- **Who sets it:** account mode → `pm_dispute_resolve` (`ban_oracle`/`ban_creator` + `…_until`); committee + mode → `pm_dispute_finalize` scales a ban by consensus on overturn. `banned_until = + time_point_sec::maximum()` ⇒ **permanent**. +- **Tokens:** the ban itself is **0** (pure status); the accompanying slash is the overturn case above. + Insurance stays locked, refundable after the ban lifts and no active markets remain. +- Bans survive snapshots and are keyed by account — re-registration cannot wipe one. **Verify:** + `pm_create_market_evaluator` (`"Oracle is banned"` / `"Creator is banned"`). **Observe:** `get_oracle` + (`banned_until`, `bans_received`), **`get_creator_ban(account)`**. + +### Bettor A — early winner + +**A** stakes **100 on side A early** (no time penalty) and wins when M resolves to A. Payout = stake + +weight-proportional share of the winners' pool. + +```mermaid +flowchart LR + A -->|pm_place_bet side=A 100| BET[(pm_bet_object
weight 100)] + BET --> M[(market M
reserves shift)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|payout 175| A +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | 100 | **175** | **+75** | +| disputed (→B) | 100 | **0** | **−100** | + +`profit = winners_pool 150 × weight 100 / Σweight 200 = 75`; no penalty → payout `100 + 75`. An overturn +makes A the **losing** side. Winnings come **only** from losers' stakes (+ forfeit pool), never emission. +**Sends:** `pm_place_bet` (`side=0`, instant); optional `pm_transfer_position` / `pm_cancel_bet`. +**Verify:** `pm_place_bet_evaluator`, `settle_market`. **Observe:** `get_account_positions` +(`expected_payout`), `get_market_weight_sums`; realized `pm_payout` in `account_history`. + +### Bettor B — loser + +**B** stakes **200 on side B**. When M resolves to **A**, B's stake funds the winners and B gets nothing. +In the disputed path B becomes the winner. + +```mermaid +flowchart LR + B -->|pm_place_bet side=B 200| BET[(pm_bet_object
status active)] + BET --> M[(market M)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|status=resolved, payout 0| BET +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | 200 | **0** | **−200** | +| disputed (→B) | 200 | **3350** | **+3150** | + +B's 200 **is** the `losers_sum` (pays the 40 fees + 150 winners' pool + LP bonus) — the parimutuel +"losers fund winners" rule. On overturn B wins and the oracle's forfeit 3000 is injected into B's pool +(`payout = 200 + 3150`). A losing bet is still recorded by a `pm_payout` with **payout=0**. **Verify:** +`settle_market` loser branch. **Observe:** `get_account_positions`, `get_market_bets`, `get_dispute`. + +### Bettor C — late winner (time penalty) + +**C** stakes **100 on side A** but **late** (T+85% of the betting window), so a **time penalty** docks +its *profit only* (not principal). Same weight as A, but earns less; the docked amount flows to the LPs. + +```mermaid +flowchart LR + C -->|pm_place_bet side=A 100
at T+85%| BET[(pm_bet_object
weight 100
time_penalty 50%)] + BET --> M[(market M)] + M ==>|grace passes| VP[[pm_auto_payout]] + VP -->|payout 138| C + VP -. penalty 37 .-> LPb[LP bonus] +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | 100 | **138** | **+38** | +| disputed (→B) | 100 | **0** | **−100** | + +`profit = 75`; `penalty = 75 × 50% = 37` (→ LPs); `payout = 100 + 75 − 37 = 138` — **−37** vs A's +75 for +the same weight. The node stamps `time_penalty` at placement from the market's penalty curve +(`time_penalty_type/value`, `penalty_curve_type`). It discourages last-second sniping and subsidises +liquidity, not the protocol. **Verify:** `pm_place_bet_evaluator` (curve eval), `compute_settlement`. +**Observe:** `get_account_positions` (`time_penalty`), `get_market_bets`. + +### Bettor D — leverage ×10 winner + +**D** opens a **×10** position: **10 collateral + 90 loan** from the [lazy pool](#the-lazy-liquidity-pool-system-object) += **100** on side A, in isolated leverage market **L** (`pm_leverage_enabled=true`, `R = 10%`). When A +wins, D keeps the upside on the full 100 after repaying loan + interest. +`pool_profit = loan 90 × R 10% = 9`; `obligation = 90 × 1.10 = 99`. + +```mermaid +flowchart LR + D -->|pm_leverage_open
collateral 10 + loan 90| POS[(pm_leverage_position
total_bet 100, obligation 99)] + POOL[(lazy pool)] -.loan 90.-> POS + POS --> L[(market L, side A)] + L ==>|settle: force_close at cancel_value| VR[[pm_leverage_resolve won=true, leverage=10]] + VR -->|min(cv,obligation) 99| POOL + VR -->|cv 200 − 99 = 101| D +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | collateral **10** | cancel_value 200 − obligation 99 = **101** | **+91** | +| disputed (→B) | collateral 10 | 0 | **−10** | + +A profitable position closes at its `cancel_value`; the pool reclaims `obligation 99` (loan 90 + **9 +interest**), D keeps the rest on just 10 of its own → **+91** (pool **+9**). Leverage settles by +liquidation, **never** via `pm_auto_payout`. Zero-sum (L): in `10 + 90 + 100 = 200`; out `101 + 99 = +200`. **Sends:** `pm_leverage_open`; optional `pm_leverage_close` (only if `cv ≥ obligation`) / +`pm_leverage_convert`. **Touched by:** `pm_leverage_resolve` (settlement force-close, +`reason=expiration`). **Verify:** `force_close_positions` → `liquidate_position(reason=2)`. **Observe:** +**`get_account_leverage_positions`** / **`get_market_leverage_positions`**, `get_lazy_pool`. + +### Bettor E — leverage ×5 liquidated + +**E** opens a **×5** position: **20 collateral + 80 loan** = **100** on side B (market **L**, `R = 10%`). +Before resolution an **opposing bet** moves the curve against B; the **cascade liquidation** force-closes +it. **E loses its collateral, but the pool is always made whole.** `obligation = 80 × 1.10 = 88`. + +```mermaid +flowchart LR + E -->|pm_leverage_open
collateral 20 + loan 80| POS[(pm_leverage_position
obligation 88)] + POOL[(lazy pool)] -.loan 80.-> POS + X -->|pm_place_bet side=A| L[(market L)] + L ==>|cascade at PRE-bet reserves
cv 88 ≤ threshold| VL[[pm_leverage_liquidate
reason=opposing_bet]] + VL -->|pool_received 88 = loan 80 + profit 8| POOL + VL -->|bettor_received 0| E +``` + +E is liquidated **before** resolution, so the final A/B result (disputed or not) doesn't change it: + +| actor | sends | receives | net | +|-------|-------|----------|-----| +| **E** | collateral **20** | **0** | **−20** | +| **pool** | loan 80 | **88** (loan 80 + R% 8) | **+8** | + +Opposing-bet liquidations run at **pre-bet** reserves where `cancel_value ≥ loan`, so `pool_received = +min(cv, obligation)` returns at least the loan — the pool **never** loses. + +> **The only path that can go negative** is a same-side **`pm_cancel_bet` (Case B)**: a cancel reverses a +> *prior, large* same-side bet (beyond the per-bet slippage cap) and, for fairness to the cancel-bettor, +> executes **first** at their submitted price — so the cascade can land `cancel_value < loan`: +> `shortfall = obligation − cancel_value`, `lazy_pool.free_balance −= shortfall`. This **bad debt** is +> **bounded** (`≤ cancel_value_before × SL%`) and **rare** (the pool's R% on every other position offsets +> it). Covered by `leverage_cancel_bet_cascade_bad_debt`. + +Pool protection is structural (`max_per_position`, `max_position_ratio`, `safety_margin`, the slippage +cap, the `expiration_buffer`). `pm_leverage_enabled=false` blocks **new** opens only — the liquidation +cascade is **not** gated by the flag, so governance can never strip the pool's protection mid-flight +(`leverage_disabled_keeps_liquidation_protection`). **Verify:** `pm_place_bet` → +`cascade_liquidate(reason=0)`; `pm_cancel_bet` → `cascade_liquidate(reason=1)`; `liquidate_position`. +**Observe:** **`get_account_leverage_positions`** (`status=1`, `pool_received`, `bettor_received`), +`get_lazy_pool`. + +### Liquidity provider in the market (LP1) + +**LP1** adds **1000** liquidity to active M (after the maker's seed). Principal is **always** returned; +on top it earns a **time-weighted** slice of the LP bonus (liquidity fee + time penalties + dust). +Distinct from a [lazy-pool provider](#liquidity-provider-in-the-lazy-pool-lz1), who deposits once and is +auto-allocated across many markets. + +```mermaid +flowchart LR + LP1 -->|pm_add_liquidity 1000| L1[(pm_liquidity_object
provider=LP1)] + L1 --> M[(market M reserves)] + M ==>|settle| SL[[settle_liquidity]] + SL -->|principal 1000 + bonus ~16| LP1 + LP1 -->|pm_withdraw_liquidity
after resolution| OUT[principal-safe exit] +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| normal (A) | 1000 | **1000 principal + ~16 bonus** | **+16** | +| disputed (→B) | 1000 | 1000 principal + ~4 bonus | **+4** | + +LP bonus pool = `liq_fee 10 + time-penalties 37 = 47`, split by `principal × seconds-in-market` (earlier +maker ~31, later LP1 ~16). The **principal guarantee** is architectural — the seed is returned before any +winner is paid; an LP can only forgo bonus, never lose principal. Withdrawing is locked from +`betting_expiration` until resolution. **Verify:** `pm_add_liquidity_evaluator` (records `deposit_time`), +`settle_liquidity` → `distribute_lp`. **Observe:** `get_market_liquidity` (`earned_fee`), +`get_market_weight_sums`. + +### The Lazy Liquidity Pool (system object) + +A **singleton** `pm_lazy_pool_object` — not an account. Depositors fund it once; the pool +**auto-allocates** a slice to each accepted market as a silent LP (`pm_liquidity_object` with empty +`provider`), **funds leverage loans**, and **recalls** idle allocations. It earns LP yield + leverage +interest, accounted MasterChef-style (one global `reward_per_share`, O(1) — see the whitepaper). +Fields: `total_shares`, `free_balance`, `allocated_balance`, `earned_balance`, `reward_per_share`, +`leverage_fund_used`. + +```mermaid +flowchart TD + LZ1 -->|pm_lazy_deposit 1000| POOL[(pm_lazy_pool
free 1000 / shares 1000)] + POOL ==>|on market accept
alloc 20% = 200| ALLOC[(pm_lazy_allocation
+ pm_liquidity provider=∅)] + ALLOC -->|market settles| YLD[route_pool_lp_return
principal 200 + yield 20] + YLD --> POOL + POOL -->|leverage loan 90| Dpos[D position] + Dpos -->|close/resolve: 90 + interest 9| POOL + POOL -. idle market .-> VR[[pm_lazy_recall]] + VR -->|step back to free| POOL +``` + +| pool money flow | effect | +|-----------------|--------| +| `pm_lazy_deposit` | `free_balance += amount`, mint shares | +| auto allocation (on accept) | `free → allocated` (silent LP) | +| market settles | `route_pool_lp_return`: principal + yield → `free`; yield → `earned` & `reward_per_share` | +| leverage open (D/E) | `free −= loan`, `leverage_fund_used += loan` | +| leverage close / resolve / opposing-bet liquidation | `min(cv, obligation) → free`; `cv ≥ loan` ⇒ **never a loss** | +| cancel-bet liquidation (Case B only) | recovers `cv` which **may be < loan** → bounded **bad debt** | +| `pm_lazy_recall` (idle market) | one 10% step of an idle allocation → `free` | +| `pm_lazy_withdraw` | burn shares → principal + pending; emergency penalty stays in pool | + +Over the canonical scenario the pool nets **+37 earned** (market M yield +20, leverage D interest +9, +leverage E opposing-bet recovery +8). As a market LP its principal is returned unconditionally; only the +*bonus* yield varies with a dispute. + +> The pool serves **both** roles from a single `free_balance`: market-LP allocations +> (`maybe_allocate_lazy`) and leverage loans (`leverage_fund_used` caps the latter). All leverage knobs +> are checked **at `pm_leverage_open` time** against the current median, so later property swings only +> affect *new* opens, never loans already out. + +VIZ in the pool is **liquid**, not vested → **no** validator-scheduling or committee-request weight. +**Exception (HF14):** for **PM committee disputes** a depositor's pool stake **is** counted — converted +to vesting-shares via `get_vesting_share_price()` and added to their `pm_dispute_vote` weight (see +[committee resolver](#resolver--committee-stake-weighted-dispute_mode--0)). **Verify:** +`apply_hardfork(CHAIN_HARDFORK_14)` (singleton), `maybe_allocate_lazy`, `route_pool_lp_return`. +**Observe:** `get_lazy_pool`. + +### Liquidity provider in the lazy pool (LZ1) + +**LZ1** deposits **1000** into the pool **once** and lets it spread across markets + leverage loans. It +earns a share of the pool's aggregate yield (`reward_per_share`), not any single market's outcome. Two +exits: **planned** (after the lock) and **emergency** (before the lock, with a penalty on *profit*). + +```mermaid +flowchart LR + LZ1 -->|pm_lazy_deposit 1000| DEP[(pm_lazy_deposit_object
shares 1000, unlock=+7d)] + DEP --> POOL[(lazy pool)] + POOL -. yield accrues .-> RPS[reward_per_share ↑] + LZ1 -->|pm_lazy_withdraw| OUT{planned or
emergency?} + OUT -->|planned, t≥unlock| P[principal 1000 + pending 29] + OUT -->|emergency, t|pm_dispute_create proposed=B
escrow fee 1000| D[(pm_dispute_object
status open)] + D --> VOTE{committee vote
or account resolve} + VOTE ==>|overturn to B| FIN[[pm_dispute_finalize / resolve]] + FIN -->|fee 1000 + bonus 2000| disp + FIN -.slash 5000 from oracle.-> SPLIT[bonus 2000 → disp
3000 → forfeit_pool → winners] +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| disputed, overturned (the "win") | dispute_fee **1000** | fee 1000 back + **bonus 2000** | **+2000** | + +`reward_target = fee × pm_dispute_reward_multiplier (3×) = 3000` → `bonus = 3000 − 1000 = 2000`, **capped +at the actual slash**; the remainder (3000) → `forfeit_pool` → the new winners. disp risked 1000, walks +away **+2000**. (Committee mode: disp does **not** vote on its own — the SHARES electorate does.) +**Verify:** `pm_dispute_create_evaluator`, overturn branch of `pm_dispute_finalize`/`pm_dispute_resolve`. +**Observe:** `get_dispute`, `get_dispute_votes`. + +### Disputer — fee forfeited (oracle upheld) + +**disp** disputes the oracle's **A** but the verdict **upholds the oracle**. The escrowed fee is +**forfeited to the oracle** as compensation, and the market settles as originally resolved (A wins). + +```mermaid +flowchart LR + disp -->|pm_dispute_create proposed=B
escrow fee 1000| D[(pm_dispute_object)] + D --> VOTE{committee vote
or account resolve} + VOTE ==>|uphold oracle A| FIN[[pm_dispute_finalize / resolve]] + FIN -->|dispute_fee 1000| orac[oracle compensation] + FIN -->|market settles as A| AUTO[[pm_auto_payout]] +``` + +| outcome | sends | receives | net | +|---------|-------|----------|-----| +| disputed, upheld (the "loss") | dispute_fee **1000** | **0** | **−1000** | + +The fee is the disputer's skin-in-the-game: a wrong/frivolous dispute pays the oracle. This asymmetry +(lose the fee if wrong, win a multiple if right) keeps the channel honest. A dispute that is never +**decided** (oracle silent / no quorum) is force-closed and the fee **returned** (net 0) — see +[dispute auto-close](#dispute-forced-to-end-anti-freeze-auto-close), which is different from losing on +the merits. **Verify:** uphold branch of `pm_dispute_finalize`/`pm_dispute_resolve`. **Observe:** +`get_dispute`, `get_oracle` (gains the fee, `disputes_won++`). + +### Resolver — committee (stake-weighted, dispute_mode = 0) + +The *whole SHARES electorate* decides by **stake-weighted vote**; no single resolver account. The verdict +is tallied deterministically by `pm_dispute_finalize` at `voting_end_time`. + +**Voting weight** = live **`effective_vesting_shares`** (`vesting − delegated + received`) **plus +lazy-pool stake converted to vesting-shares**, since many DAO members park VIZ in the pool (where it is +liquid): + +``` +pool_claim_viz = pool_NAV × deposit.shares / pool.total_shares +pool_weight = pool_claim_viz × get_vesting_share_price() +voter_weight = effective_vesting_shares + pool_weight +``` + +The participation quorum denominator is likewise `total_vesting_shares + (pool_NAV → vesting-shares)`. The +7-day deposit lock prevents deposit-vote-withdraw gaming. + +```mermaid +flowchart LR + V1[voter · eff_vshares] -->|pm_dispute_vote outcome,percent| D[(pm_dispute_votes)] + V2[voter · eff_vshares] -->|pm_dispute_vote| D + D ==>|voting_end_time| FIN[[pm_dispute_finalize]] + FIN -->|argmax rshares,
threshold check| VERDICT{uphold / overturn} + VERDICT -->|consensus_strength
scales slash & bans| OUT[settle] +``` + +- **Sends (voters):** `pm_dispute_vote` — **auth `regular`**. `vote_outcome = -1` upholds, else proposes + the correct outcome; `vote_percent ∈ [-10000, 10000]`. A voter may **revise** their ballot any number + of times while voting is open — a repeat vote **overwrites** the prior (latest wins, no "Already voted"). +- **No commit-reveal — deliberate, will NOT change.** A committee dispute is an **open public hearing**: + the running tally is visible (`get_dispute_votes`) and votes are not hidden. The DAO's value + proposition is resolving disputes as truthfully and transparently as possible; new evidence surfaces + during voting and voters are *expected* to update; and voters are **not paid** for matching the + majority, so the usual anti-herding (beauty-contest) rationale for commit-reveal does not apply. + +| actor | sends | receives | +|-------|-------|----------| +| each voter | 0 | **0** — voting is a governance duty, not a paid action | + +Voters never receive tokens; influence is pure stake weight. Economic flows land on the disputer, oracle, +and bettors per the [disputed master ledger](#master-ledger--disputed-resolve-oracle-said-a--overturned-to-b). +Niche markets may fail the threshold → fall through to +[dispute auto-close](#dispute-forced-to-end-anti-freeze-auto-close). **Verify:** `pm_dispute_vote_evaluator` +(modify-or-create on `by_market_voter`); `pm_dispute_finalize` (`lazy_vote_weight`, `get_vesting_share_price`, +quorum, argmax, `consensus_strength`). **Observe:** `get_dispute_votes` (live tally + finalize projection: +`quorum_percent_bp`, `expected_uphold`, `expected_outcome`, `expected_consensus_strength_bp`). Tests: +`committee_dispute_lazy_pool_voting_weight`, `committee_dispute_flips_outcome`. + +### Resolver — single account (centralized, dispute_mode = 1) + +The market names one `dispute_resolver` account (e.g. a regulator multisig) that decides alone — **no +stake weight, no DAO vote**. Set at creation, it must differ from both `oracle` and `creator` (anti +self-judging). Same op set as committee mode; only *who decides* differs. + +```mermaid +flowchart LR + disp -->|pm_dispute_create| D[(dispute, mode=1)] + resolver -->|pm_dispute_resolve
correct_outcome=B
penalty_amount, ban flags| FIN[[verdict]] + FIN -->|slash penalty_amount| orac[oracle.insurance ↓] + FIN -->|fee + reward| disp + FIN --> AUTO[[pm_auto_payout settles B]] +``` + +- **Sends:** `pm_dispute_resolve` — **auth `active` of the named `dispute_resolver`** only: + `correct_outcome`, `penalty_amount` (insurance to slash — a fixed amount, **not** stake-scaled), + `ban_oracle`/`ban_creator` (+ `…_until`). + +| actor | sends | receives | +|-------|-------|----------| +| resolver | 0 | **0** — a neutral arbiter | + +The post-verdict canon is identical to committee mode; only the slash size differs (resolver-set +`penalty_amount`, no `consensus_strength` scaling, since there is a single decider). KYC/whitelisting of +the resolver is a **client-layer** concern. **Verify:** `pm_dispute_resolve_evaluator` (only the named +resolver, `dispute_mode==1`). **Observe:** `get_dispute`, `get_oracle`, **`get_creator_ban(account)`**. + +### Dispute forced to end (anti-freeze auto-close) + +A dispute that is **never decided** — oracle silent and (committee) no quorum — cannot freeze the market +forever. At `auto_close_time` the `pm_dispute_auto_close` processor force-ends it: **everyone is +refunded**, the disputer's fee is **returned**, and the unresponsive oracle is penalised. No winner is +picked. + +```mermaid +flowchart LR + disp -->|pm_dispute_create
escrow fee 1000| D[(dispute, status open)] + D -. oracle silent / no quorum .-> WAIT[auto_close_time reached] + WAIT ==>|VIRTUAL| AC[[pm_dispute_auto_close]] + AC -->|refund all bets| bettors + AC -->|fee 1000 back| disp + AC -->|insurance slash → DAO| orac +``` + +| actor | sends | receives | net | +|-------|-------|----------|-----| +| A / B / C | bet | full refund | **0** | +| maker / LP1 | liquidity | principal back | **0** (no bonus) | +| disp | dispute_fee 1000 | **1000 back** | **0** | +| orac | insurance −slash → DAO | — | **− slash** | + +This is **not** "the disputer lost": a returned fee (net 0) differs from a forfeited fee +([disputer-loser](#disputer--fee-forfeited-oracle-upheld), net −1000). Nobody profits; the market is +voided to break the freeze, the cost falls on the oracle that didn't respond. The same void-and-refund +shape covers `pm_oracle_missed_penalty` and `pm_no_contest`. Tune `pm_dispute_auto_close_sec` (14 d) vs +`pm_dispute_vote_period_sec` (3 d) so honest disputes resolve first. **Verify:** `process_pm_markets` +auto-close scan (`refund_all_bets` + `return_liquidity` + fee credit; `disputes_auto_closed++`). +**Observe:** `get_dispute` (status → auto-closed), `get_market`, `get_oracle`. diff --git a/docs/protocol/operations/overview.md b/docs/protocol/operations/overview.md index 6b695bc64d..41fa9eeffd 100644 --- a/docs/protocol/operations/overview.md +++ b/docs/protocol/operations/overview.md @@ -52,6 +52,32 @@ These are user-initiated operations that can be broadcast to the network. | 58 | `use_invite_balance_operation` | active | [Invites](./invites.md) | | 60 | `fixed_award_operation` | regular | [Awards](./awards.md) | | 61 | `target_account_sale_operation` | master | [Account Market](./account-market.md) | +| 64 | `set_reward_sharing_operation` | active | [Validators](./validators.md) | +| 66 | `pm_oracle_register_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 67 | `pm_oracle_update_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 68 | `pm_create_market_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 69 | `pm_oracle_accept_market_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 70 | `pm_place_bet_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 71 | `pm_commit_bet_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 72 | `pm_reveal_bet_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 73 | `pm_cancel_bet_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 74 | `pm_add_liquidity_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 75 | `pm_withdraw_liquidity_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 76 | `pm_resolve_market_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 77 | `pm_no_contest_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 78 | `pm_dispute_create_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 79 | `pm_dispute_vote_operation` | regular | [Prediction Markets](./prediction-markets.md) | +| 80 | `pm_dispute_resolve_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 81 | `pm_transfer_position_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 82 | `pm_lazy_deposit_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 83 | `pm_lazy_withdraw_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 91 | `pm_leverage_open_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 92 | `pm_leverage_close_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 93 | `pm_leverage_convert_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 98 | `pm_dispute_oracle_respond_operation` | active | [Prediction Markets](./prediction-markets.md) | +| 99 | `pm_unban_operation` | active | [Prediction Markets](./prediction-markets.md) | + +> IDs are the fixed index in the chain's single `operation` variant (append-only). Gaps in this table are **virtual** operations (below) interleaved by ID — e.g. 62–63, 65, 84–90, 94–97, 100. --- @@ -83,6 +109,19 @@ Virtual operations are generated by the blockchain itself during block processin | 59 | `expire_escrow_ratification_operation` | Escrow deadline missed | [Virtual Ops](../virtual-operations.md) | | 62 | `bid_operation` | Auction bid placed | [Virtual Ops](../virtual-operations.md) | | 63 | `outbid_operation` | Auction outbid | [Virtual Ops](../virtual-operations.md) | +| 65 | `stakeholder_reward_operation` | Reward-sharing payout to a stakeholder | [Validators](./validators.md) | +| 84 | `pm_batch_settle_operation` | Batch epoch boundary settled | [Prediction Markets](./prediction-markets.md) | +| 85 | `pm_commit_forfeit_operation` | Commit-reveal escrow forfeited (unrevealed) | [Prediction Markets](./prediction-markets.md) | +| 86 | `pm_auto_payout_operation` | Market settled (per-market payout marker) | [Prediction Markets](./prediction-markets.md) | +| 87 | `pm_dispute_finalize_operation` | Committee vote tallied | [Prediction Markets](./prediction-markets.md) | +| 88 | `pm_dispute_auto_close_operation` | Dispute anti-freeze auto-close | [Prediction Markets](./prediction-markets.md) | +| 89 | `pm_oracle_missed_penalty_operation` | Oracle missed resolution deadline | [Prediction Markets](./prediction-markets.md) | +| 90 | `pm_lazy_recall_operation` | Lazy-pool graduated recall step | [Prediction Markets](./prediction-markets.md) | +| 94 | `pm_leverage_liquidate_operation` | Leveraged position liquidated | [Prediction Markets](./prediction-markets.md) | +| 95 | `pm_leverage_resolve_operation` | Leveraged position settled at resolution | [Prediction Markets](./prediction-markets.md) | +| 96 | `pm_market_accepted_operation` | Market went live (oracle accepted / self / auto) | [Prediction Markets](./prediction-markets.md) | +| 97 | `pm_payout_operation` | Per-bettor parimutuel payout | [Prediction Markets](./prediction-markets.md) | +| 100 | `pm_ban_expired_operation` | Temporary oracle/creator ban lapsed | [Prediction Markets](./prediction-markets.md) | --- diff --git a/docs/protocol/operations/prediction-markets.md b/docs/protocol/operations/prediction-markets.md index bf977a9d18..33e93e533f 100644 --- a/docs/protocol/operations/prediction-markets.md +++ b/docs/protocol/operations/prediction-markets.md @@ -106,6 +106,8 @@ Creates a market; the creator seeds the first liquidity and becomes the first LP Oracle accepts (`status → active`) or rejects (liquidity refunded to creator; `status → deleted`) a pending market. On accept the oracle **quotes its actual terms** via `oracle_fee_percent` + `oracle_fixed_fee` — each must be `≤` the creator's offer on the market and `oracle_fee_percent ≤ pm_max_oracle_fee_percent`. The quote is **frozen onto the market** and a `pm_market_accepted` virtual op is emitted (so history parsers see the launch + terms). Settlement later reads only these frozen fields — never the live median. +The oracle must act within `pm_oracle_accept_window_sec` (default 1h) of creation. If it does neither by the market's `accept_deadline`, the per-block cron voids the market (`status → deleted`), refunds the creator's seed liquidity (**not** the non-refundable creation fee), and emits `pm_market_expired` (see Virtual operations). + | Field | Type | Description | |-------|------|-------------| | `market_id` | `int64` | Pending market | @@ -155,7 +157,14 @@ Withdraws liquidity (principal-safe). Locked from `betting_expiration` until res ### `pm_resolve_market_operation` (ID 76) **Auth:** `active` of `oracle` -Oracle resolves to `winning_outcome`. Opens the dispute grace window (`result_expiration + pm_dispute_grace_sec`); after it elapses `pm_auto_payout` settles. +Oracle resolves to `winning_outcome`. Opens the dispute grace window (`result_expiration + pm_dispute_grace_sec`); after it elapses `pm_auto_payout` settles. The oracle's resolution statement is **stored on the market** (like an oracle's `rules_url`) so a client can read it directly via `get_market` without scanning history. + +| Field | Type | Description | +|-------|------|-------------| +| `market_id` | `int64` | Target market | +| `winning_outcome` | `int16_t` | Winning outcome index | +| `decision_url` | `string` | Evidence link, `≤ MAX_PM_DECISION_URL_LEN`; stored on the market | +| `decision_reason` | `string` | Free-text justification, `≤ MAX_PM_DISPUTE_REASON_LEN`; stored on the market (`decision_reason`) | ### `pm_no_contest_operation` (ID 77) **Auth:** `active` of `oracle` @@ -177,7 +186,39 @@ A committee dispute is an **open public hearing** — there is **no commit-revea ### `pm_dispute_resolve_operation` (ID 80) **Auth:** `active` of `resolver` -Account-mode verdict by the market's configured `dispute_resolver`. May slash `penalty_amount` of insurance and ban the oracle/creator until the given times. +Account-mode verdict by the market's configured `dispute_resolver`. May slash `penalty_amount` of insurance and ban the oracle/creator until the given times (`ban_*_until = time_point_sec::maximum()` = permanent). + +> **Bans are a compliance/regulator feature, exclusive to account mode.** When a market routes its disputes to an account-mode `dispute_resolver` (e.g. a regulator or a licensed arbitrator), that resolver can sanction **both the oracle and the market creator** — temporarily or permanently — in the same verdict, on top of the insurance slash: it lets a regulator serving as resolver bar a bad-faith oracle or a repeat-offender creator from the platform. **Committee/DAO mode (`dispute_mode == 0`) has no ban power by design** — it is a transparent public hearing that only slashes insurance and adjusts reputation (`pm_dispute_finalize`), never bans. A ban set here records the issuing `resolver` in the target's `banned_by`, so only that resolver may lift it early via `pm_unban`; otherwise it lapses at `banned_until` (the cron emits `pm_ban_expired`). + +| Field | Type | Description | +|-------|------|-------------| +| `market_id` | `int64` | Disputed market | +| `correct_outcome` | `int16_t` | Final correct outcome (`-1` = void/no-contest) | +| `penalty_amount` | `asset` (VIZ) | Oracle insurance to slash | +| `ban_oracle` / `ban_oracle_until` | `bool` / `time_point_sec` | Ban the oracle until the given time | +| `ban_creator` / `ban_creator_until` | `bool` / `time_point_sec` | Ban the creator from creating markets until the given time | + +### `pm_dispute_oracle_respond_operation` (ID 98) +**Auth:** `active` of `oracle` + +The market's oracle posts a **public rebuttal** onto an open dispute. Because a dispute is a public hearing, the text is stored on the dispute object (`oracle_response` / `oracle_response_time`, readable via `get_dispute`) so every voter/resolver can weigh it. Allowed only while the dispute is open and `now ≤ oracle_response_deadline`; re-posting overwrites the previous response. + +| Field | Type | Description | +|-------|------|-------------| +| `market_id` | `int64` | Disputed market | +| `response` | `string` | Rebuttal text, non-empty, `≤ MAX_PM_DISPUTE_REASON_LEN` | + +### `pm_unban_operation` (ID 99) +**Auth:** `active` of `resolver` + +Lifts a ban imposed by an account-mode `pm_dispute_resolve` **early**. Only the account recorded in the target's `banned_by` (the resolver that set the ban) may lift it; at least one of `unban_oracle` / `unban_creator` must be set, and the corresponding ban must currently be active. Sets `banned_until` to a past time and clears `banned_by`. (Bans not lifted here simply expire at `banned_until` — the cron then emits `pm_ban_expired`.) + +| Field | Type | Description | +|-------|------|-------------| +| `resolver` | `account_name_type` | The account that imposed the ban (must equal the target's `banned_by`) | +| `target` | `account_name_type` | The banned oracle / creator | +| `unban_oracle` | `bool` | Clear the oracle ban (`pm_oracle_object.banned_until`) | +| `unban_creator` | `bool` | Clear the creator ban (`pm_creator_ban_object.banned_until`) | ### `pm_transfer_position_operation` (ID 81) **Auth:** `active` of `from` @@ -213,9 +254,12 @@ Emitted by the PM consensus logic — either by a signed operation's evaluator ( | 95 | `pm_leverage_resolve_operation` | Settlement — leveraged position force-closed: `outcome_index`, `won`, `pool_received`/`bettor_received`, `leverage` | | 96 | `pm_market_accepted_operation` | Evaluator — market went live: oracle accepted, self-oracle, or auto-accept; frozen oracle terms + `self_oracle` | | 97 | `pm_payout_operation` | Settlement — per active bet: `amount` (stake), `side`/`outcome_index`, `payout` (**0 on a loss**) | +| 100 | `pm_ban_expired_operation` | A temporary oracle/creator ban lapsed at `banned_until`: the cron cleared it (fields `account`, `oracle`, `creator`). Early manual lifts use the signed `pm_unban` instead | +| 101 | `pm_market_expired_operation` | A pending market's `accept_deadline` passed: the oracle never accepted/rejected within `pm_oracle_accept_window_sec` — market voided, seed refunded (creation fee kept). Fields `oracle`, `creator`, `market_id`, `refunded_liquidity` | > IDs 91–93 are the *regular* ops `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert` (see the -> spec). Per-bettor results are `pm_payout`; the per-market `pm_auto_payout` remains a settlement marker. +> spec); IDs 98–99 are the *regular* ops `pm_dispute_oracle_respond`/`pm_unban` (above). Per-bettor +> results are `pm_payout`; the per-market `pm_auto_payout` remains a settlement marker. --- diff --git a/docs/protocol/operations/validators.md b/docs/protocol/operations/validators.md index 5089314540..599325782a 100644 --- a/docs/protocol/operations/validators.md +++ b/docs/protocol/operations/validators.md @@ -163,4 +163,39 @@ Delegates all validator voting to a proxy account. All existing direct votes are --- +## `set_reward_sharing_operation` (ID 64) + +**Auth:** `active` of `owner` + +**HF13 validator reward sharing.** A validator opts to forward a fraction of its block reward to its **stakeholders** — the accounts that voted for it — proportionally to time-weighted vote weight. `sharing_rate` is that fraction in basis points; the shared pool accumulates and is distributed at each epoch end via the `stakeholder_reward` virtual op. + +| Field | Type | Description | +|-------|------|-------------| +| `owner` | `account_name_type` | Validator setting its sharing rate | +| `sharing_rate` | `uint16_t` | Fraction of block reward forwarded to stakeholders, in basis points (0 = none, 10000 = 100%) | + +```json +[64, { + "owner": "alice", + "sharing_rate": 2500 +}] +``` + +- `sharing_rate` is capped at 10000 (100%). +- Distribution is by **time-weighted** vote weight, so recently-added votes earn a smaller share until they mature. + +--- + +## `stakeholder_reward_operation` (ID 65) — virtual + +Emitted at each distribution epoch when a validator with a non-zero `sharing_rate` pays out a stakeholder's share of the shared block reward. Virtual (never signed); appears in `account_history`. + +| Field | Type | Description | +|-------|------|-------------| +| `validator` | `account_name_type` | The validator that shared the reward | +| `stakeholder` | `account_name_type` | The voter receiving a share | +| `shares` | `asset` (SHARES) | Amount credited to the stakeholder | + +--- + See also: [Data Types](../data-types.md), [Operations Overview](./overview.md), [Chain Properties](../../governance/chain-properties.md). diff --git a/docs/protocol/virtual-operations.md b/docs/protocol/virtual-operations.md index 6e38c7b3d7..b0ac23d5b7 100644 --- a/docs/protocol/virtual-operations.md +++ b/docs/protocol/virtual-operations.md @@ -351,4 +351,32 @@ All locked funds return to `from`. --- +## Prediction Markets (HF14) + +Emitted by the PM consensus logic — **not** a wall-clock cron. Two sources: +- A **signed operation's evaluator**, at the instant it applies — `pm_market_accepted` (on accept / self-oracle / auto-accept) and `pm_leverage_liquidate` (on an opposing- or cancel-bet that pushes a leveraged position under threshold). +- The **deadline processor `process_pm_markets()`**, run each block: it settles markets that have reached an **expiration / deadline / dispute-grace / epoch boundary** (bounded at `pm_processing_cap_per_block`, oldest-deadline-first). + +See [Prediction Market Operations](./operations/prediction-markets.md). (IDs 91–93 are the *regular* ops `pm_leverage_open`/`pm_leverage_close`/`pm_leverage_convert`, and IDs 98–99 the *regular* ops `pm_dispute_oracle_respond`/`pm_unban` — see that page.) + +| ID | Operation | Trigger | +|----|-----------|---------| +| 84 | `pm_batch_settle_operation` | Epoch boundary reached: queued bets executed on the epoch-open snapshot | +| 85 | `pm_commit_forfeit_operation` | `reveal_deadline` passed unrevealed: penalty → `forfeit_pool`, rest refunded | +| 86 | `pm_auto_payout_operation` | Dispute grace elapsed (per market): parimutuel settlement + LP principal returned | +| 87 | `pm_dispute_finalize_operation` | Committee `voting_end_time` reached: tally decides; oracle penalty; re-resolved/upheld | +| 88 | `pm_dispute_auto_close_operation` | `auto_close_time` reached, oracle never responded: anti-freeze refund, insurance slashed → DAO | +| 89 | `pm_oracle_missed_penalty_operation` | `result_expiration` passed unresolved: insurance slashed → DAO, all bets refunded | +| 90 | `pm_lazy_recall_operation` | An idle lazy-pool allocation reached a recall step: one graduated step returned to the pool | +| 94 | `pm_leverage_liquidate_operation` | Evaluator — mid-market leverage liquidation (opposing-bet `0` / cancel-bet `1` cascade) | +| 95 | `pm_leverage_resolve_operation` | Settlement — leveraged position force-closed at `cancel_value`: `outcome_index`, `won`, `pool_received`/`bettor_received`, `leverage` | +| 96 | `pm_market_accepted_operation` | Evaluator — market went live: oracle accepted, self-oracle, or auto-accept; frozen oracle terms + `self_oracle` | +| 97 | `pm_payout_operation` | Settlement — per active bet: `amount` (stake), `side`/`outcome_index`, `payout` (**0 on a loss**); alongside the per-market `pm_auto_payout` | +| 100 | `pm_ban_expired_operation` | A temporary oracle/creator ban lapsed at `banned_until`: the cron cleared it (`account`, `oracle`, `creator`). Early manual lifts use the signed `pm_unban` instead | +| 101 | `pm_market_expired_operation` | `accept_deadline` passed on a pending market: the named oracle never accepted/rejected within `pm_oracle_accept_window_sec` — market voided (`status -1`), seed liquidity refunded (`refunded_liquidity`), creation fee **not** refunded (`oracle`, `creator`, `market_id`, `refunded_liquidity`) | + +All PM money movement is strictly zero-sum (no emission); settlement conserves `Σ out == Σ bets + LP principal + forfeit_pool`. + +--- + See also: [Operations Overview](./operations/overview.md), [Awards](./operations/awards.md), [Committee](./operations/committee.md). diff --git a/package-lock.json b/package-lock.json index 9e2c142b61..bb428d08db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,9 @@ "name": "viz-ledger-docs", "version": "1.0.0", "devDependencies": { - "vitepress": "^1.5.0" + "mermaid": "^11.4.1", + "vitepress": "^1.5.0", + "vitepress-plugin-mermaid": "^2.0.17" } }, "node_modules/@algolia/abtesting": { @@ -270,6 +272,20 @@ "node": ">= 14.0.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -320,6 +336,20 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@docsearch/css": { "version": "3.8.2", "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", @@ -371,17 +401,6 @@ } } }, - "node_modules/@docsearch/js/node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "loose-envify": "^1.1.0" - } - }, "node_modules/@iconify-json/simple-icons": { "version": "1.2.83", "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.83.tgz", @@ -399,6 +418,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@iconify/utils": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.3.tgz", + "integrity": "sha512-LPKOXPn/zV+zis1oOfGWogaXVpqUybF3ZS6SCZIsz8vg0ivVp9+fVqyYB7xq0aiST/VhUQYGO1qo6uoYSiEJqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -406,6 +437,41 @@ "dev": true, "license": "MIT" }, + "node_modules/@mermaid-js/mermaid-mindmap": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/mermaid-mindmap/-/mermaid-mindmap-9.3.0.tgz", + "integrity": "sha512-IhtYSVBBRYviH1Ehu8gk69pMDF8DSRqXBRDMWrEfHoaMruHeaP2DXA3PBnuwsMaCdPQhlUUcy/7DBLAEIXvCAw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@braintree/sanitize-url": "^6.0.0", + "cytoscape": "^3.23.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.1.0", + "d3": "^7.0.0", + "khroma": "^2.0.0", + "non-layered-tidy-tree-layout": "^2.0.2" + } + }, + "node_modules/@mermaid-js/mermaid-mindmap/node_modules/@braintree/sanitize-url": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", + "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.1.tgz", + "integrity": "sha512-VuHdsYMK1bT6X2JbcAaWAhugTRvRBRyuZgd+c22swUeI9g/ntaxF7CY7dYarhZovofCbUNO0G7JesfmNtjYOCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.1" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", @@ -763,6 +829,290 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -770,6 +1120,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -797,10 +1154,10 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/prop-types": { - "version": "15.7.15", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", - "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "dev": true, "license": "MIT", "optional": true @@ -826,6 +1183,17 @@ "dev": true, "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/@vitejs/plugin-vue": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", @@ -1104,20 +1472,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "debug": "4" - }, - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/algoliasearch": { "version": "5.52.1", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.52.1.tgz", @@ -1145,14 +1499,6 @@ "node": ">= 14.0.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, - "license": "MIT", - "optional": true - }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -1163,21 +1509,6 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -1211,20 +1542,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -1236,6 +1553,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/copy-anything": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", @@ -1252,6 +1579,16 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -1259,34 +1596,564 @@ "dev": true, "license": "MIT" }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", "dev": true, "license": "MIT", - "optional": true, + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dev": true, + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dev": true, + "license": "ISC", "dependencies": { - "ms": "^2.1.3" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" }, "engines": { - "node": ">=6.0" + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "dev": true, + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "dev": true, + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dev": true, + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.4.0" + "license": "MIT" + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" } }, "node_modules/dequal": { @@ -1313,20 +2180,14 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" } }, "node_modules/emoji-regex-xs": { @@ -1336,58 +2197,16 @@ "dev": true, "license": "MIT" }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", - "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "node_modules/es-toolkit": { + "version": "1.48.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.48.1.tgz", + "integrity": "sha512-wfnXlwd5I75eXRtdD2vuEs50xHHESECDsGD7yiQnfFVNoa5522NwXEbmgo98LfiukSQHs+mBM7/YG3qKJB9/mQ==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.6", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - } + "workspaces": [ + "docs", + "benchmarks" + ] }, "node_modules/estree-walker": { "version": "2.0.2", @@ -1407,46 +2226,6 @@ "tabbable": "^6.4.0" } }, - "node_modules/follow-redirects": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", - "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1462,116 +2241,12 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, - "license": "MIT", - "optional": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } + "license": "MIT" }, "node_modules/hast-util-to-html": { "version": "9.0.5", @@ -1629,19 +2304,38 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "agent-base": "6", - "debug": "4" + "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { - "node": ">= 6" + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" } }, "node_modules/is-what": { @@ -1657,28 +2351,53 @@ "url": "https://github.com/sponsors/mesqueeb" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true, - "license": "MIT", - "optional": true - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", "dev": true, + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], "license": "MIT", - "optional": true, "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" + "commander": "^8.3.0" }, "bin": { - "loose-envify": "cli.js" + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" } }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==", + "dev": true + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -1696,15 +2415,17 @@ "dev": true, "license": "MIT" }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", "dev": true, "license": "MIT", - "optional": true, + "bin": { + "marked": "bin/marked.js" + }, "engines": { - "node": ">= 0.4" + "node": ">= 20" } }, "node_modules/mdast-util-to-hast": { @@ -1729,6 +2450,37 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/mermaid": { + "version": "11.15.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.15.0.tgz", + "integrity": "sha512-pTMbcf3rWdtLiYGpmoTjHEpeY8seiy6sR+9nD7LOs8KfUbHE4lOUAprTRqRAcWSQ6MQpdX+YEsxShtGsINtPtw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "es-toolkit": "^1.45.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/micromark-util-character": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", @@ -1823,31 +2575,6 @@ ], "license": "MIT" }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/minisearch": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", @@ -1862,14 +2589,28 @@ "dev": true, "license": "MIT" }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "node_modules/non-layered-tidy-tree-layout": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz", + "integrity": "sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw==", "dev": true, "license": "MIT", "optional": true }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "dev": true, + "license": "MIT" + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -1884,6 +2625,24 @@ "dev": true, "license": "ISC" }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", @@ -1954,17 +2713,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "dev": true, - "license": "MIT", - "optional": true, - "engines": { - "node": ">=10" - } - }, "node_modules/regex": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", @@ -1999,6 +2747,13 @@ "dev": true, "license": "MIT" }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "dev": true, + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.60.4", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", @@ -2044,6 +2799,33 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, "node_modules/search-insights": { "version": "2.17.3", "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", @@ -2098,6 +2880,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "dev": true, + "license": "MIT" + }, "node_modules/superjson": { "version": "2.2.6", "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", @@ -2118,6 +2907,16 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -2129,13 +2928,15 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/undici-types": { - "version": "7.24.6", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", - "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", "dev": true, "license": "MIT", - "optional": true + "engines": { + "node": ">=6.10" + } }, "node_modules/unist-util-is": { "version": "6.0.1", @@ -2210,6 +3011,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -2246,6 +3061,7 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -2772,6 +3588,20 @@ } } }, + "node_modules/vitepress-plugin-mermaid": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/vitepress-plugin-mermaid/-/vitepress-plugin-mermaid-2.0.17.tgz", + "integrity": "sha512-IUzYpwf61GC6k0XzfmAmNrLvMi9TRrVRMsUyCA8KNXhg/mQ1VqWnO0/tBVPiX5UoKF1mDUwqn5QV4qAJl6JnUg==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@mermaid-js/mermaid-mindmap": "^9.3.0" + }, + "peerDependencies": { + "mermaid": "10 || 11", + "vitepress": "^1.0.0 || ^1.0.0-alpha" + } + }, "node_modules/vitepress/node_modules/@shikijs/core": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", diff --git a/package.json b/package.json index 6d46ca9ee5..17e716bfe2 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,15 @@ "predev": "npm run sync-l10n", "prebuild": "npm run sync-l10n", "prepreview": "npm run sync-l10n", + "prestart": "npm run sync-l10n", "dev": "vitepress dev docs", "build": "vitepress build docs", "preview": "vitepress preview docs", "start": "npm run dev" }, "devDependencies": { - "vitepress": "^1.5.0" + "vitepress": "^1.5.0", + "vitepress-plugin-mermaid": "^2.0.17", + "mermaid": "^11.4.1" } }