sync(audit): port toly upstream/master 3969c31..2052807 to fork-engine — 6 of 7 audit-validated SF ports + ENG-PORT-4 fixup - #90
Conversation
…te — closes ENG-PORT-1 (CRITICAL-5) Empty-market gate from toly upstream (toly-engine:10250-10292) — refuses live insurance withdrawal unless market is provably empty + fully accrued. ADAPTED port: 3 toly-only conditions (active_close_present, stress_consumed_bps_e9_since_envelope, bankruptcy_hmax_lock_active) SKIPPED per KL-FORK-ENGINE-BANKRUPT-CLOSE-1 + KL-FORK-ENGINE-STRESS-ENVELOPE-1 (fork has no such fields). 8 surviving conditions all map to existing fork RiskEngine fields: - oi_eff_long_q == 0 && oi_eff_short_q == 0 (no live OI) - stored_pos_count_long == 0 && stored_pos_count_short == 0 (no stored positions) - stale_account_count_long == 0 && stale_account_count_short == 0 (no stale accounts) - neg_pnl_account_count == 0 (no negative-PnL accounts) - current_slot == last_market_slot (market accrued) Without this gate, an admin holding insurance_authority can drain the live insurance fund while users hold positions whose maintenance margin assumes that insurance is there. With 8 conditions enforced, the fund is only drainable when the market has no live obligations. Cross-ref: ENGINE_BODY_DIFF.md §Vault accounting Hunk 1 (CRITICAL severity), AUDIT_WORK_PRESERVATION.md row 1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… 4-predicate form) — ENG-PORT-3 prerequisite Adds toly's account_has_unsettled_live_effects predicate (toly-engine:4884) as a post-touch invariant detector for sync_account_fee_to_slot_not_atomic, execute_trade, withdraw, convert_released_pnl, and close_account. ADAPTED port per KL-FORK-ENGINE-B-TRACKING-1: toly's 5th predicate (B-snap mismatch via b_target_for_account) is SKIPPED — fork has no B-tracking subsystem. The 4 retained predicates (epoch / A / K / F) catch K/F/A_basis/epoch staleness; the residual narrow case (B drift with epoch/A/K/F in sync) is documented in KL-FORK-ENGINE-B-TRACKING-1. This is the helper-definition commit; callsite wiring (4 callsites) follows in subsequent commits as Port 2 (sync_account_fee) and Port 3-callsites lands on the 4 trade/withdraw/ convert/close paths. Cross-ref: ENGINE_BODY_DIFF.md §execute_trade_not_atomic Hunk 2; AUDIT_WORK_PRESERVATION.md row 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…w_does_not_precede_loss helper — closes ENG-PORT-2 (CRITICAL-6)
Closes the loss-juniority bug in sync_account_fee_to_slot_not_atomic. Without these
changes, the fee cursor could advance past last_market_slot and charge fees for slots
the market hasn't accrued through, ranking the fee draw senior to losses that should
have been booked first.
Three sub-changes:
1. New helper ensure_fee_draw_does_not_precede_loss (toly-engine equivalent):
- Rejects with Undercollateralized if pnl < 0 (realized loss already on the account)
- On Live, also rejects if account_has_unsettled_live_effects (epoch/A/K/F drift —
unrealized loss not yet booked). ADAPTED 4-predicate form per
KL-FORK-ENGINE-B-TRACKING-1; B-snap predicate dropped (no fork B-tracking subsystem).
2. live_loss_safe_anchor block in sync_account_fee_to_slot_not_atomic:
- When Live + now_slot > last_market_slot + account holds position basis, anchor
to last_market_slot instead of now_slot.
- Threaded through both shape_anchor (validation) and the final commit anchor.
3. Loss-juniority guard call when fee_rate_per_slot > 0 && shape_anchor > last_fee_slot.
Plus a defensive narrowing of the live envelope check to fire only on now_slot >
current_slot (avoids redundant envelope check on repeated same-slot fee syncs).
Cross-ref: ENGINE_BODY_DIFF.md §sync_account_fee_to_slot_not_atomic (CRITICAL severity);
AUDIT_WORK_PRESERVATION.md row 2.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…— closes ENG-PORT-3 (CRITICAL-7) Adds the post-touch invariant guard at 4 fork-engine entrypoints that mutate account state through touch_account_live_local. Each guard rejects with Undercollateralized before any downstream mutation that would otherwise operate on stale K/F/A_basis/epoch. - execute_trade_not_atomic: both counterparties checked after the dual touch (Step 11). Prevents trade attaching at stale basis when a side mode change post-touch leaves K/F drifted. - withdraw_not_atomic: post-touch, before finalize_touched (which sweeps fees against capital using current K/F). - convert_released_pnl_not_atomic: post-touch, before convert_released_pnl_core mutates released-PnL bookkeeping. - close_account_not_atomic: post-touch, before effective_pos_q_checked (which derives from A_basis/K snaps) is used in the position==0 check. Defense-in-depth post-touch invariant — does not fire on any current happy-path because touch_account_live_local correctly settles snaps. Catches regressions in the touch machinery before they propagate to the trade/withdraw/convert/close mutation paths. ADAPTED 4-predicate form per KL-FORK-ENGINE-B-TRACKING-1; B-snap predicate absent (no fork B-tracking subsystem). Residual narrow case (B drift with epoch/A/K/F in sync) closes when the B-tracking subsystem ports. Cross-ref: ENGINE_BODY_DIFF.md §execute_trade_not_atomic Hunk 2 (HIGH severity); AUDIT_WORK_PRESERVATION.md row 3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ew account_gate helper — closes ENG-PORT-4 (CRITICAL-8) Closes the maker-replaces-taker hazard in enforce_side_mode_oi_gate. The pre-port aggregate-OI gate alone admits a trade where one counterparty closes long (releasing OI) while the other opens long (consuming the same OI) — net aggregate OI change is zero, so the side-mode (DrainOnly / ResetPending) check passes, even though a NEW account just landed on a side mid-reset. Three sub-changes (toly-engine equivalent at toly-engine:8021-8063): 1. New helper enforce_side_mode_account_gate(old_eff, new_eff). Verbatim port. Rejects any new entrant onto a ResetPending side regardless of OI movement; on DrainOnly, rejects unless the account is reducing its existing same-side basis. 2. enforce_side_mode_oi_gate widens to 6-arg signature with per-account positions (old_eff_a, new_eff_a, old_eff_b, new_eff_b, oi_long_after, oi_short_after). Body keeps the existing aggregate-OI checks AND adds the account-gate calls for both counterparties. 3. The single execute_trade_not_atomic callsite now passes the per-account positions captured earlier in the trade flow. The gate is additive: it widens what gets rejected without weakening what gets accepted. All happy paths are unaffected. Test callsites in tests/proofs_*.rs (#![cfg(kani)]-gated) need the 4-arg expansion — follow-up commit "ENG-PORT-4 fixup" updates them. Cross-ref: ENGINE_BODY_DIFF.md §enforce_side_mode_oi_gate Hunk 1 (HIGH severity); AUDIT_WORK_PRESERVATION.md row 4. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ignature — ENG-PORT-4 fixup Mechanical update — ENG-PORT-4 widened the gate signature from 2-arg (oi_long_after, oi_short_after) to 6-arg (old_eff_a, new_eff_a, old_eff_b, new_eff_b, oi_long_after, oi_short_after). Without these test updates the engine library still builds clean, but proofs_*.rs Kani harnesses fail to compile. - proofs_invariants.rs:580/585/594/599/607 (proof_side_mode_gating): pass 0i128 per-account positions; the test isolates the aggregate-OI gate, and the per-account gate is a no-op when new_eff == 0. - proofs_instructions.rs:596: pass per-account positions in (b, a) order matching the bilateral_oi_after call directly above. - proofs_liveness.rs:69, 79: pass (old_a, new_a, old_b, new_b) in scope. - proofs_checklist.rs:393: pass (eff_a_before, new_eff_a, eff_b_before, new_eff_b) in scope. Verification: - cargo build --release: clean (lib unchanged from ENG-PORT-4 production commit). - Kani build verification deferred to the post-port verification gate (cargo kani --workspace, run after all 7 ports + cleanup commit). Cross-ref: AUDIT_WORK_PRESERVATION.md row 5 (the original 38b551e fixup commit had a 4-file pattern; this fixup matches plus adds proofs_checklist.rs:393 which the prior session also touched). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-zero, dust-zero ADAPTED, drain-guard) — closes ENG-PORT-5a (CRITICAL-9) 4 SF hunks from toly upstream's resolve_market_not_atomic body (toly-engine:9518-9674). 2 sibling toly hunks (ensure_no_active_bankrupt_close, clear_stress_envelope) are SKIPPED per KL parents — fork has neither subsystem. Hunks ported: 1. Hunk 2 — pre-state snapshot. 8 locals (pre_mode_long/short, pre_a_long/short, pre_oi_long/short, pre_stored_long/short) capture the pre-resolve view BEFORE any mutation. Downstream gates read snapshots so per-side decisions reflect the pre-resolve state. 2. Hunk 3 — K-terminal-delta zero-on-zero-OI. A side with no pre-resolve OI cannot accumulate a non-zero terminal delta (would attribute a settlement shift to non-existent positions). Forces delta to 0 when pre_oi_<side> == 0. 3. Hunk 5 — phantom-dust zero on pre_stored_<side> == 0. ADAPTED to fork's single-bound schema per KL-PHANTOM-DUST-SCHEMA-1 (toly uses certified+ potential pair; fork uses single-bound phantom_dust_bound_<side>_q). Semantically equivalent: no stored positions ⇒ no dust to track. 4. Hunk 6 — drain-reset pre_stored guard. Only enter drain reset when pre_stored_<side> > 0. The pre-port code unconditionally entered drain even on sides with zero stored positions, performing spurious side-mode transitions. Hunks SKIPPED (per parent KL — see KEEP_LIST): - Hunk 1 (ensure_no_active_bankrupt_close) — KL-FORK-ENGINE-BANKRUPT-CLOSE-1 (no fork bankrupt-close subsystem; deferred to ENG-PORT-5b). - Hunk 4 (clear_stress_envelope) — KL-FORK-ENGINE-STRESS-ENVELOPE-1 (no fork stress-envelope subsystem; deferred to ENG-PORT-5b). Cross-ref: ENGINE_BODY_DIFF.md §resolve_market_not_atomic Hunks 2/3/5/6 (SECURITY-FORWARD HIGH/MEDIUM); AUDIT_WORK_PRESERVATION.md row 6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ly-return — closes Port 6 SF guard from toly upstream's force_close_resolved_with_fee_not_atomic body. Refuses terminal close when reconcile_resolved_not_atomic left position_basis_q non-zero — guards against partial-progress reconcile (e.g., social loss spread that didn't fully unwind basis). Without this guard, close_resolved_terminal_not_atomic silently mis-accounts the residual, OR fails later in a less recoverable way. Toly's matching fee-charging line (sync_account_fee_to_slot at resolved_slot) is FEATURE-DIVERGENCE per ENGINE_BODY_DIFF Hunk 3 — SKIPPED. Fork doesn't charge maintenance fees at resolved close. Cross-ref: ENGINE_BODY_DIFF.md §force_close_resolved_not_atomic Hunk 2 (SECURITY-FORWARD HIGH); audit-validated independent of bankruptcy/stress/active-close subsystems. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…offsets.rs example — restore cargo test clean Pre-existing build break in fork-only example examples/detailed_offsets.rs (added in 9340977 "chore: add detailed_offsets example for layout verification"). Both fields were removed without rename in the v12.1 sync (9a1c627 + 84a9293): - last_crank_slot retired; conceptually replaced by last_market_slot (already printed on a separate line in this file) - gc_cursor retired in 84a9293 ("Simplify ADL: replace windowed crank with full-scan") Cleanest fix per Phase 0 baseline analysis: delete the 2 broken lines, leaving the rest of the example intact (it correctly references current RiskEngine fields incl. fork-only phantom_dust_bound_<side>_q, materialized_account_count, etc.). Restores `cargo test --features test` to clean (was failing at compile due to E0609 errors in the example), so the post-Phase-1 verification gate uses the standard SESSION_START_CHECKLIST baseline command without the --lib --tests workaround. Per BULK_SYNC_PLAN.md Phase 0 finding #3: applied as final cleanup commit on the sync branch before push. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThis pull request strengthens the RiskEngine's invariant enforcement by introducing post-touch guards that detect Live-mode account inconsistencies, hardening fee juniority and side-mode account-level entrant restrictions across withdraw, trade, convert, close, fee synchronization, and market resolution operations. ChangesRisk Engine Invariant Guards & Side-Mode Hardening
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/percolator.rs`:
- Around line 4924-4935: The predicate in account_has_unsettled_live_effects
currently flags adl_a_basis drift as an unsettled live effect, but adl_a_basis
is intentionally left stale by settle_side_effects_live unless q_eff_new == 0;
remove the adl_a_basis comparison to avoid false positives. Specifically, in
account_has_unsettled_live_effects (which already checks position_basis_q,
adl_epoch_snap vs get_epoch_side, and compares adl_k_snap/get_k_side and
f_snap/get_f_side), drop the check "account.adl_a_basis !=
self.get_a_side(side)"; alternatively, if you prefer to keep the check, update
the live-touch path in settle_side_effects_live to fully refresh/rewrite
position_basis_q and adl_a_basis before these guards run. Ensure references to
adl_a_basis, adl_k_snap, f_snap, adl_epoch_snap, position_basis_q,
settle_side_effects_live, get_a_side, get_k_side, get_f_side, and get_epoch_side
are used to locate the changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6251db56-8e10-4e85-8694-4b032b2e216f
📒 Files selected for processing (6)
examples/detailed_offsets.rssrc/percolator.rstests/proofs_checklist.rstests/proofs_instructions.rstests/proofs_invariants.rstests/proofs_liveness.rs
| fn account_has_unsettled_live_effects(&self, idx: usize) -> Result<bool> { | ||
| let account = &self.accounts[idx]; | ||
| if account.position_basis_q == 0 { | ||
| return Ok(false); | ||
| } | ||
| let side = side_of_i128(account.position_basis_q).ok_or(RiskError::CorruptState)?; | ||
| if account.adl_epoch_snap != self.get_epoch_side(side) { | ||
| return Ok(true); | ||
| } | ||
| Ok(account.adl_a_basis != self.get_a_side(side) | ||
| || account.adl_k_snap != self.get_k_side(side) | ||
| || account.f_snap != self.get_f_side(side)) |
There was a problem hiding this comment.
adl_a_basis drift is a false positive here.
In this engine, same-epoch ADL shrink is represented lazily: settle_side_effects_live refreshes adl_k_snap/f_snap/adl_epoch_snap, but it does not rewrite position_basis_q or adl_a_basis unless q_eff_new == 0. That makes account.adl_a_basis != self.get_a_side(side) a normal post-touch state, not an inconsistency. With this predicate in place, any account that has seen ordinary ADL shrink will start tripping the new guards and get blocked from withdraw/trade/convert/close and recurring fee sync even though its state is otherwise canonical. Either drop the A-basis comparison here, or make the live-touch path fully reattach basis/A snapshots before these guards run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/percolator.rs` around lines 4924 - 4935, The predicate in
account_has_unsettled_live_effects currently flags adl_a_basis drift as an
unsettled live effect, but adl_a_basis is intentionally left stale by
settle_side_effects_live unless q_eff_new == 0; remove the adl_a_basis
comparison to avoid false positives. Specifically, in
account_has_unsettled_live_effects (which already checks position_basis_q,
adl_epoch_snap vs get_epoch_side, and compares adl_k_snap/get_k_side and
f_snap/get_f_side), drop the check "account.adl_a_basis !=
self.get_a_side(side)"; alternatively, if you prefer to keep the check, update
the live-touch path in settle_side_effects_live to fully refresh/rewrite
position_basis_q and adl_a_basis before these guards run. Ensure references to
adl_a_basis, adl_k_snap, f_snap, adl_epoch_snap, position_basis_q,
settle_side_effects_live, get_a_side, get_k_side, get_f_side, and get_epoch_side
are used to locate the changes.
Summary
Bulk sync of toly upstream
aeyakovenko/percolatormaster intodcccrypto/percolatormain, executed via per-fn manual port (Option 3 from the bulk-sync strategy) after the naivegit mergeapproach catastrophically violated KL deferrals on attempt #1 (see~/wrapper-engine-deep-audit/SYNC_FAILURE_LOG.md).Closes 6 of 7 audit-validated security ports + 1 deferred. Includes ENG-PORT-4 test fixup + 1 cleanup commit.
Source / target
upstream/masterat2052807("Fix terminal epoch recovery close", 2026-05-09)origin/mainatc2a8a9a(PR #88 v12.19-engine sync, 2026-04-28)3969c31("Update full Kani audit timings", 2026-04-25)feat/sync/engine-bulk-2026-05git mergewas abandonedPR #88 already absorbed 96 toly commits through
3969c31via itsmerge: upstream/master @ 3969c31 (v12.19 baseline) into fork maincommit, so this sync is the remaining 88 toly commits — most of which are documentation, Kani test refinements, or KL-deferred subsystem evolution.Commits (9 total, in chronological order)
7ef8be7withdraw_live_insurance_not_atomicempty-market gate486381daccount_has_unsettled_live_effectshelper60bedc6sync_account_feeloss-safe anchor +ensure_fee_draw_does_not_precede_loss61938f20206a59enforce_side_mode_oi_gate6-arg +enforce_side_mode_account_gatee2f1c67fe1d6fdresolve_market_not_atomic4 SF hunks (pre-state, K-zero, dust ADAPTED, drain-guard)7d7689fforce_close_resolved_not_atomicposition-basis early-returndac8bf6last_crank_slot+gc_cursorrefs fromexamples/detailed_offsets.rs(pre-existing build break)ADAPTED ports per KEEP_LIST
Three commits applied adapted ports because toly's hunks reference KL-deferred subsystems:
account_has_unsettled_live_effects(commit 2) — toly's 5-predicate form drops to 4-predicate (epoch / A / K / F) per KL-FORK-ENGINE-B-TRACKING-1. The B-snap predicate is SKIPPED; fork has no B-tracking subsystem.withdraw_live_insurance_not_atomic(commit 1) — toly's 11-condition gate drops to 8 conditions per KL-FORK-ENGINE-BANKRUPT-CLOSE-1 + KL-FORK-ENGINE-STRESS-ENVELOPE-1. The 3 SKIPPED conditions reference fields fork doesn't have (active_close_present,stress_consumed_bps_e9_since_envelope,bankruptcy_hmax_lock_active).resolve_market_not_atomic(commit 7) — toly's certified+potential phantom-dust zero block ADAPTS to fork's single-bound schema (phantom_dust_bound_<side>_q = 0instead of clearing the toly-side 4 fields) per KL-PHANTOM-DUST-SCHEMA-1.Deferred (with documentation)
credit_account_from_insurance_not_atomiclive_reconciliation_lock_active2052807 "Fix terminal epoch recovery close"b_epoch_snap/loss_weight_sum/social_loss_remainder_*machinery. SKIPS for fork.record_uninsured_protocol_lossdurable accumulatorexplicit_unallocated_protocol_loss+explicit_unallocated_loss_saturatedfields to RiskEngine, which would BREAK the NFT compile-timeEXPECTED_RISK_ENGINE_SIZEassertion. SKIP per NFT_BASELINE.md.ensure_no_active_bankrupt_close/loss_stale_positive_pnl_lock_active/stress_gate_activechecks (~7 hunks across multiple fns)Verification (all 5 gates green at HEAD)
cargo build --releasecargo test --features test(FULL — incl. examples after cleanup)scripts/verify-engine-preservation.shscripts/verify-engine-deferred-absent.shscripts/verify-eng-port-preservation.shNFT regression check (Phase 1.3):
cargo build --releaseon~/percolator-nftsucceeded — all 54 compile-timesize_of!/offset_of!assertions hold. Symbol-level diff identical to~/wrapper-engine-deep-audit/NFT_BASELINE.diff.txt(0 regression lines).Out of scope (deferred subsystems)
Per
~/wrapper-engine-deep-audit/KEEP_LIST.md:active_close_*fields +ensure_no_active_bankrupt_closehelper +bankruptcy_hmax_lock_active. Deferred to ENG-PORT-5b.stress_consumed_bps_e9_since_envelope+stress_envelope_*fields +clear_stress_envelopehelper. Deferred to ENG-PORT-5b.b_target_for_account+account_has_unsettled_b+record_uninsured_protocol_lossdurable accumulator +social_loss_remainder_*machinery +proofs_b_index.rstest file.RiskError::RecoveryRequiredvariant +EngineRecoveryRequiredPercolatorError (KL-FORK-ENGINE-BANKRUPT-CLOSE-1) — orphan with parent.trading_fee_bpsmodel.Test plan
cargo build --release— cleancargo test --features test— 335/0/0 (full, no--lib --testsworkaround needed)cargo kani --workspace) — deferred to post-PR-review verification gate; reviewer can run before mergecargo buildfor~/percolator-prog(wrapper) against this engine — confirms wrapper still builds via path dep (Phase 2 verification)Cross-references
~/wrapper-engine-deep-audit/AUDIT_WORK_PRESERVATION.md— confirms the prior session's 5 ENG-PORT commits are subsumed~/wrapper-engine-deep-audit/SYNC_FAILURE_LOG.md— records why naivegit mergewas abandoned~/wrapper-engine-deep-audit/FORK_INVENTORY.md§5 — conflict-zone table~/wrapper-engine-deep-audit/KEEP_LIST.md— 34 KL-* groups (the audit's semantic rules; per-hunk porting honors each one)~/wrapper-engine-deep-audit/NFT_BASELINE.md— regression-detection baseline (passed)🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests