Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/rules/predict-contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ Predict-specific Move rules: package architecture, config and capability shapes,
- Predict order IDs are scoped by `(expiry_market_id, order_id)`. Do not encode or infer market lifecycle facts such as expiry from the order ID; bind an order to a market through `predict_account` position keys (`PositionKey`) and the market/exposure state that created it.
- Mint-admission policy must not be part of packed order decoding or structural `Order` validation. Future upgrades to mint-only policy, such as price thresholds, must not retroactively make existing packed order IDs invalid.
- `PoolVault.active_expiry_markets` contains only expiries that still contribute pool valuation or risk. The pool-coordinated settled-market sweep must deactivate the expiry as it returns free cash and materializes terminal accounting; do not expose an expiry-only terminal path that can strand pool capital. The [design decisions](../../packages/predict/docs/design/decisions.md) own this mechanism.
- After a settled-market sweep, the expiry may retain only payout backing and its inventory-impact escrow. Return free cash and unused fee incentives to `PoolVault`; live rebalancing must not run for expired or settled markets.
- After a settled-market sweep, the expiry may retain only payout backing. Return all cash above it and unused fee incentives to `PoolVault`; live rebalancing must not run for expired or settled markets.
- Model Predict positions as binary range contracts. Live value is range probability value times quantity; settled value is the full quantity for a winner and zero for a loser. There is no floor, no financing, and no liquidation: leverage was removed, so an order's only durable terms are its strike range, quantity, and sequence.
- Terminology: docs use options/structured-product vocabulary (canonical glossary: `packages/predict/docs/glossary.md`). The mint-economics identifiers are `premium` and `min_premium` (and the `OrderMinted.premium` event field); the holder pays the contract's full entry value, so premium equals entry value and no separate `entry_value` identifier exists. The EWMA congestion surcharge keeps core's `penalty` vocabulary in code.
- Mint slippage bounds must name what they control. Both mint entrypoints cap the all-in account withdrawal through `max_cost`; quantity mint may disable it and separately cap probability, while budget mint requires it and uses `min_quantity` against `max_premium` instead of a duplicate probability argument. Keep a one-use payment sum inline, and keep `OrderMinted` payment components separate. The [response-policy register](../../packages/predict/predeploy/response-policies.md) RP-19 owns the rationale and pinning tests.
Expand Down
1 change: 1 addition & 0 deletions .claude/rules/predict-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Read this before editing the Predict local development system under `packages/pr
- Python tests: `python3 -m unittest discover -s harness/tests -p 'test_*.py' -v` and `python3 -m unittest discover -s simulations/tests -p 'test_*.py' -v` from `packages/predict/`.
- Validate behavior with a real localnet run (`python3 -m harness live --traders N --seconds S`) or a scoped campaign, then `python3 -m harness analyze`. Run these in the **main loop or background, never a blocking subagent** (long runs trip watchdogs). Retention/teardown model: README.
- **On a PTB abort, read the real VM error, not the framework tag.** A `MovePrimitiveRuntimeError` in `0x2::dynamic_field::borrow_child_object` names only the framework fn; the true cause is the dry-run `executionErrorSource` in the saved `artifacts/failed_transactions/*.json` (now also printed live by `runtime.ts` and by the `analyze` bug oracle). The C-1 object-cache ceiling was chased for days off the truncated framework string while that field read `Object runtime cached objects limit (1000 entries) reached` all along — the trace's `errorTag` only kept its first 120 chars.
- **`executionErrorSource` can be absent, and then the framework tag is not evidence.** Under the gRPC executor the dry run may return only a structured `status.error` with `cleverError: "[Undefined]"`, so `runtime.ts` prints nothing and the analyzer cannot corroborate a declared wall — `p32-refresh-gas-2026-08-20.md` records a whole run with the field empty on all 100 artifacts. Do not read `dynamic_field:0` as either cause on its own: the object-cache limit and a genuine missing field share that location, and the gRPC transport labels both `MoveAbort` with `abortCode` 0. Discriminate by running the same call over a large table in `sui move test`, which enforces no object-cache limit: a structural read error aborts there at any size, the VM ceiling never does.
- **`sui move test` does NOT enforce Sui execution-layer limits.** The object-runtime cached-objects cap (1,000 dynamic-field children/tx), per-tx gas, and max object size are full-node checks — a unit test loads 1,100+ children without aborting. Reproduce these on localnet (a harness strategy), never in `sui move test`.
- **Never blind-`rm` `.localnets/instances/` while a run may be live.** A bare `rm -rf` deletes a *running* campaign's dir out from under it — its keeper/updater then `ENOENT` on every write and the trace is lost. Check `python3 -m harness status` first (non-empty slots = a live run), then use the **slot-aware** `cleanup --instances`, which skips any dir whose run-id is an active slot.

Expand Down
8 changes: 3 additions & 5 deletions .claude/skills/predict-audit/evals/seeds.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,10 @@ verify panel refuted a real bug. Record misses; they are the highest-signal inpu
- Bug: a supplier now mints ≥ fair shares (rounds UP), diluting incumbent LPs — a ROUNDING_POLICY R2 violation
(user-facing outflows/shares must round in the protocol's favor). Expect the invariants lens (R2) to flag it.

### S2 — solvency guard dropped (impact escrow unbacked) · lens 01 / lens 06 · expect High/Critical
### S2 — solvency guard dropped · lens 01 / lens 06 · expect High/Critical
- File: `packages/predict/sources/expiry_cash.move`, `assert_backing`.
- Replace `cash.required_cash(payout_liability)` with `payout_liability` (drops the `+ inventory_impact_reserve` term).
- Bug: backing now covers payout but NOT the isolated inventory-impact escrow, so cash can fall below
`payout_liability + inventory_impact_reserve` — the exact invariant this module documents, and a live close
can then be paid out of cash that backs a winner. Expect a solvency/assertion finding.
- Delete the `assert!(cash.balance() >= payout_liability, EInsufficientCash);` line.
- Bug: cash can fall below payout liability, so a later payout can be paid from unbacked cash. Expect a solvency/assertion finding.

### S3 — removed version gate on a state mutator · lens 04 access-control · expect High
- File: `packages/predict/sources/expiry_market.move`, `assert_live_mint_allowed`.
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/predict-audit/evals/verify_corpus.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@
{
"id": "refute-assert-backing-off-by-one",
"expect": "refuted",
"why": "The code is correct: assert_backing checks balance >= required_cash(payout_liability) where required_cash = payout_liability + inventory_impact_reserve. The invariant is an inclusive lower bound; there is no off-by-one \u2014 dust never aborts by ROUNDING_POLICY R1 (reserve >= payout by construction).",
"why": "The code is correct: assert_backing checks balance >= payout_liability. The invariant is an inclusive lower bound; there is no off-by-one \u2014 dust never aborts by ROUNDING_POLICY R1 (cash >= payout by construction).",
"severity": "Medium",
"title": "assert_backing uses >= where it should use > , allowing a one-ulp shortfall",
"location": "packages/predict/sources/expiry_cash.move:62",
"claim": "The backing assert admits balance exactly equal to required cash, leaving zero margin and risking a downstream underflow.",
"scenario": "balance == payout_liability + inventory_impact_reserve exactly; a later dust subtraction underflows.",
"scenario": "balance == payout_liability exactly; a later dust subtraction underflows.",
"impact": "correctness",
"confidence": "low",
"recommendation": "Tighten the compare to strictly greater.",
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/predict-audit/lenses/01-invariants.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Produce:
it (solvency / accounting-consistency / ordering / conservation); judge whether the code maintains it
across ALL touching flows. Give special weight to **cross-module and cross-package** invariants — a
property established in one module/package and silently relied on in another (e.g. `expiry_cash`'s
`cash_balance >= payout_liability + inventory_impact_reserve`; the exact `current_nav` mark used identically for PLP
`cash_balance >= payout_liability`; the exact `current_nav` mark used identically for PLP
supply and withdraw; the packed-order-ID `quantity` round-trip — mint insert must add bit-equal what remove subtracts).

2. **ECONOMIC FLOW MAP** — trace every path where value (DUSDC, PLP) enters, moves, or leaves:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ new adversarial scenarios and property/fuzz tests.**

**Required campaigns (write new scenarios; assert invariants after every step):**
1. **Solvency / conservation fuzz.** Randomized sequences of mint / redeem / liquidate / supply / withdraw over
the python mirrors. After each op assert: cash-backing (`balance >= payout_liability + inventory_impact_reserve`),
the python mirrors. After each op assert: cash-backing (`balance >= payout_liability`),
no negative balances, DUSDC conserved across the trader/LP/protocol/builder split, rounding favors the
protocol (never the user). Report any breaking seed + the minimal reproducing sequence.
2. **NAV mark symmetry / timing.** Drive supply and withdraw across a flush at a moving mark; confirm
Expand Down
4 changes: 2 additions & 2 deletions .claude/skills/predict-audit/primer.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ DUSDC (settlement/custody for all trading + payouts, and the sponsored fee-incen
- `builder_code.move` — fee-attribution object; accrues + claims builder fees.
- `order.move` — packs immutable position terms (absolute boundary ticks, quantity, sequence) into a u256 order id (132 dense bits); validates shape.
- `expiry_market.move` — per-expiry risk engine; mint / live redeem / settled redeem / settlement / compaction state machine; routes DUSDC; produces per-expiry `current_nav`.
- `expiry_cash.move` — raw DUSDC custody arithmetic; enforces `cash_balance >= payout_liability + inventory_impact_reserve`.
- `expiry_cash.move` — raw DUSDC custody arithmetic; enforces `cash_balance >= payout_liability`.
- `ewma.move` — gas-congestion surcharge ("EWMA penalty") added to trade fees.
- `constants.move` — upgrade-only constants/sentinels (version, scalings, `pos_inf_tick`, resolution period).
- `pricing/pricing.move` — the live pricing boundary: binds the market's underlying to current propbook feeds, pre-expiry live-pricing check, feed freshness, the pricing-safe surface envelope (forward>0, basis, |rho|<=1, sigma band), SVI variance + normal-CDF binary pricing; settlement read.
Expand Down Expand Up @@ -86,7 +86,7 @@ DUSDC (settlement/custody for all trading + payouts, and the sponsored fee-incen
`market_manager` cadence config → `create_and_share_expiry_market` (reads no live spot; absolute ticks snapshotted from cadence) → seed propbook Pyth + BS data for the emitted expiry → `mint` → live trade/redeem (partial or full close) → **passive settlement** (terminal spot = the exact post-expiry Pyth print from propbook minute history; if absent, the market stays unsettled and live valuation aborts) → settled redeem → compaction (free storage). Full-pool valuation: a transaction-local `PoolValuation` snapshots active expiries, values each once under the valuation lock; the **privileged** flush prices PLP supply AND withdraw at one exact `current_nav` mark.

## Glossary (neutral)
absolute tick = strike unit; `raw = tick * tick_size`. `pos_inf_tick`/`neg_inf` = open-ended-range sentinels. winner payout = the full `Q` (leverage was removed 2026-08-14; there is no floor). The only pre-settlement conservatism is the aggregate disjoint-backing λ buffer (D030). payout_liability / settled_payout_liability = cash the market must back. inventory_impact_reserve = isolated escrow of collected inventory-impact charges. EWMA penalty = gas-congestion fee surcharge. basis = forward/spot from BS pushes. SVI = volatility-surface parameterization for the binary tail. NAV = pool value pricing PLP shares; the flush mark is the **exact** `current_nav` (tree `walk_linear`, floored), no conservative band. float_scaling = 1e9 fixed-point.
absolute tick = strike unit; `raw = tick * tick_size`. `pos_inf_tick`/`neg_inf` = open-ended-range sentinels. winner payout = the full `Q` (leverage was removed 2026-08-14; there is no floor). The only pre-settlement conservatism is the aggregate disjoint-backing λ buffer (D030). payout_liability / settled_payout_liability = cash the market must back. inventory-impact charges are ordinary expiry cash (D034); there is no isolated escrow. EWMA penalty = gas-congestion fee surcharge. basis = forward/spot from BS pushes. SVI = volatility-surface parameterization for the binary tail. NAV = pool value pricing PLP shares; the flush mark is the **exact** `current_nav` (tree `walk_linear`, floored), no conservative band. float_scaling = 1e9 fixed-point.

## Prior-awareness (mandatory)
Before raising anything, read and apply the [Predict development-system authority order](../../../packages/predict/predeploy/README.md#authority-order). Do not duplicate an existing open item or re-litigate a rejected direction unless its recorded revisit condition is met.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Recon-style invariant testing), mapped to Predict surfaces. Lens 01 builds the l

## 1. Solvency / conservation
The protocol must always be able to pay what it owes; value is conserved across parties.
- `expiry_cash`: `cash_balance >= payout_liability + inventory_impact_reserve` after EVERY cash mutation.
- `expiry_cash`: `cash_balance >= payout_liability` after EVERY cash mutation.
- Live backing for a winning order is the exact `quantity - floor_shares` (= its settled payout under the static floor) plus the aggregate disjoint-backing λ buffer (D030) — not a time-varying max-live term.
- DUSDC is conserved across trader / LP / protocol / builder — no path mints value from nothing or strands it.
- LP NAV: the exact `current_nav` mark prices PLP supply AND withdraw identically (`supply_NAV == withdraw_NAV
Expand Down
53 changes: 51 additions & 2 deletions packages/predict/devtools/ts/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1340,10 +1340,11 @@ function addRedeem(tx: Transaction, params: RedeemParams): void {
tx.pure.u256(BigInt(params.orderId)),
tx.pure.u64(params.closeQuantity),
// `min_probability` then `min_proceeds` close-side slippage floors; the
// benchmark never sets a floor, so pass 0 to disable both (mirrors mint's
// U64_MAX caps).
// The benchmark disables close floors and permits any inventory-impact
// debit if a hedge-removing close exceeds its gross proceeds.
tx.pure.u64(0),
tx.pure.u64(0),
tx.pure.u64(U64_MAX),
// `redeem_live` loads the account and ambient-settles it (`settle<DUSDC>`)
// before crediting the payout, so it reads the singleton AccumulatorRoot at 0xacc.
tx.object(ACCUMULATOR_ROOT_ID),
Expand Down Expand Up @@ -1505,6 +1506,22 @@ export function bindFeedsToUnderlyingTx(params: { pythFeedId: string }): Transac
return tx;
}

export function setTemplateInventoryImpactMaxRateTx(
protocolConfigId: string,
maxRate: bigint,
): Transaction {
const tx = new Transaction();
tx.moveCall({
target: target("protocol_config", "set_template_inventory_impact_max_rate"),
arguments: [
tx.object(protocolConfigId),
tx.object(ADMIN_CAP_ID),
tx.pure.u64(maxRate),
],
});
return tx;
}

export function setTemplateExpiryFeeConfigTx(
protocolConfigId: string,
expiryFeeWindowMs: bigint,
Expand Down Expand Up @@ -1612,6 +1629,38 @@ export function createExpiryMarketTx(params: {
return tx;
}

// Same create, plus a mass-check of an off-chain 1% ratio ladder before the
// market is shared. The keeper uses this when the template rate is nonzero.
// Requires a live surface for the deployable expiry already on the bound
// feeds (RP-24: written in a prior tx).
export function createExpiryMarketWithInventoryTx(params: {
poolVaultId: string;
protocolConfigId: string;
lifecycleCapId: string;
cadenceId: number;
ratios: bigint[];
} & OracleFeedIds): Transaction {
const tx = new Transaction();
tx.moveCall({
target: target("registry", "create_and_share_expiry_market_with_inventory_grid"),
arguments: [
tx.object(REGISTRY_ID),
tx.object(params.poolVaultId),
tx.object(params.protocolConfigId),
tx.object(ORACLE_REGISTRY_ID),
tx.object(params.pythFeedId),
tx.object(params.bsValueStoreId),
tx.object(params.bsSviStoreId),
tx.object(params.lifecycleCapId),
tx.pure.u32(PREDICT_ORACLE_ID),
tx.pure.u8(params.cadenceId),
tx.pure(bcs.vector(bcs.u64()).serialize(params.ratios)),
tx.object(CLOCK_ID),
],
});
return tx;
}

// Fund / rebalance one expiry's cash from pool idle toward target. Standalone and
// permissionless; this is what makes a freshly created market mintable. Replaces
// the old setup-only PLP sync.
Expand Down
Loading