Skip to content

feat(roundfi-core): commit-reveal escape-valve listings (#232)#322

Merged
alrimarleskovar merged 2 commits into
mainfrom
feat/escape-valve-commit-reveal-232
May 15, 2026
Merged

feat(roundfi-core): commit-reveal escape-valve listings (#232)#322
alrimarleskovar merged 2 commits into
mainfrom
feat/escape-valve-commit-reveal-232

Conversation

@alrimarleskovar
Copy link
Copy Markdown
Owner

Closes the on-chain half of #232 — previously flagged in docs/security/mev-front-running.md § 2.2 as the single non-bounded extraction vector in the user-facing surface.

Summary

Two new instructions form a hash-commit / time-cooldown protocol around the existing escape-valve listing flow. The price is hidden during commit; revealed prices arm a 30s cooldown before the listing is buyable; the legitimate buyer (who has the salt off-chain) gets a deterministic head-start over searchers reacting to the public reveal-tx logs.

What ships

New instructions

Instruction What it does
escape_valve_list_commit(commit_hash) Creates EscapeValveListing in Pending status. Only SHA-256(price ‖ salt) is on chain — searchers can't derive the price. Same eligibility constraints as legacy escape_valve_list.
escape_valve_list_reveal(price, salt) Validates (price, salt) against commit_hash, transitions Pending → Active, sets buyable_after = now + REVEAL_COOLDOWN_SECS (30s). Authority: only listing.seller.

Modified instructions

  • escape_valve_buy — new check: now >= listing.buyable_after (error: ListingNotBuyableYet). No-op for legacy listings (buyable_after = listed_at).
  • escape_valve_list — gated by config.commit_reveal_required. When the flag is on, returns CommitRevealRequired; when off, sets commit_hash = [0;32] + buyable_after = listed_at (preserves devnet/demo single-step UX).
  • update_protocol_config — new Option<bool> field for new_commit_reveal_required.

State additions

// EscapeValveListing (+40 bytes, 99 → 139)
+ commit_hash:    [u8; 32]
+ buyable_after:  i64

// EscapeValveStatus
+ Pending = 3

// ProtocolConfig (+1 byte from forward-compat pad)
+ commit_reveal_required: bool   // default false; flipped post-canary

Constants

+ REVEAL_COOLDOWN_SECS: i64 = 30   // ~75 slots; tunable via redeploy

Errors

+ CommitRevealRequired    + ListingNotPending
+ InvalidCommitHash       + ListingNotBuyableYet

SDK (sdk/src/onchain-raw.ts)

  • LISTING_ACCOUNT_SIZE: 99 → 139
  • RawListingView gains commitHash: Buffer + buyableAfter: bigint
  • LocalListingStatus adds "pending"

Docs

  • docs/security/mev-front-running.md § 2.2 — mitigation status flipped from 🔵 pending → 🟡 commit-reveal shipped on-chain; Jito bundling demoted to operator-side recommendation (not strictly required given the on-chain cooldown enforces a deterministic head-start). § 3 summary table + big-picture paragraph updated to reflect bounded-by-cooldown posture; escape_valve_buy no longer the single non-bounded vector.

Why commit-reveal + cooldown (not either alone)

Commit-reveal alone would shift the snipe race from listing-creation to reveal moment. Cooldown alone (with the price still public at list time) doesn't help — searcher and buyer both wait, both race after. Together: price hidden until reveal and buyer has a fixed window post-reveal to land first. 30s is the canary default — large enough for a UI buyer to react via the front-end, small enough that listings don't feel stale.

Validation

cargo check -p roundfi-core    # green (35 warnings, all pre-existing
                                 anchor-debug cfg warnings)
pnpm typecheck                  # green (workspace)
pnpm lint                       # green
pnpm test:parity                # 7 passing
pnpm test:app-encoders          # 58 passing
pnpm test:events                # 2 passing
pnpm test:economic-parity-l1    # 45 passing

anchor build (SBF target) remains blocked by the mpl-core ↔ Anchor 1.0 borsh coexistence issue tracked in PR #319 — same posture as every recent on-chain PR. Host-side cargo check validates the code shape; runtime testing via bankrun arrives when #319 unblocks.

Follow-ups (deliberately out of scope)

Item Why not here
App-side TS encoders for escape_valve_list_commit / escape_valve_list_reveal Frontend wiring is product surface, not protocol; separate PR.
Bankrun coverage of commit + reveal + cooldown + buy Gated on the bankrun harness restoration tracked under #319.
Operator-side Jito bundling for reveal + buy Mainnet operational layer, not on-chain. Recommended at mainnet rampup.
MAINNET_READINESS.md § 5.4 status flip (🟡 → ✅) Will land in a doc sync after both this PR and #320 (also touching that doc) merge.

Test plan

  • Host-side cargo check green
  • Workspace typecheck + lint green
  • All 4 non-bankrun TS test suites green (112 tests total)
  • CI passes
  • Manual review of the commit-reveal flow + cooldown semantics
  • Bankrun coverage (when feat(toolchain): Agave 2.x migration (#230) — BLOCKED on upstream mpl-core #319 unblocks): commit → reveal-wrong-salt fail → reveal-right → buy-during-cooldown fail → buy-after-cooldown success

Generated by Claude Code

Closes the on-chain half of the listing-race MEV vector — the single
non-bounded extraction surface flagged in
docs/security/mev-front-running.md § 2.2.

What ships
==========

Two new instructions form a hash-commit / time-cooldown protocol around
the existing escape-valve listing flow:

  escape_valve_list_commit(commit_hash)
    Creates an `EscapeValveListing` PDA in `Pending` status. Only
    `SHA-256(price_le_bytes || salt_le_bytes)` is on chain — searchers
    monitoring escape-valve flow cannot derive the price and cannot
    prepare a snipe tx. Same eligibility constraints as the legacy
    `escape_valve_list` (non-defaulted, current on contributions,
    pool Active, no existing listing for the slot).

  escape_valve_list_reveal(price_usdc, salt)
    Validates the (price, salt) pair against the stored hash, transitions
    the listing to `Active`, and sets
    `buyable_after = now + REVEAL_COOLDOWN_SECS` (30s). Authority: only
    `listing.seller` may reveal — third parties cannot trigger reveals
    even if they somehow learn the salt.

`escape_valve_buy` now enforces `now >= listing.buyable_after` (new
error: `ListingNotBuyableYet`). The legitimate buyer (who learns
`(price, salt)` off-chain) has a deterministic head-start to land their
buy tx the moment the cooldown lapses; searchers reacting to the
public reveal-tx logs cannot land a buy inside the cooldown window.

The legacy single-step `escape_valve_list` remains. It now sets
`commit_hash = [0;32]` and `buyable_after = listed_at` — preserves
devnet/demo UX (immediately buyable). Mainnet ops can disable the
legacy path by setting `config.commit_reveal_required = true` via
`update_protocol_config` once the canary validates commit-reveal.

Why commit-reveal + cooldown (vs. either alone)
==============================================

- Commit-reveal alone would just shift the snipe race from listing-
  creation moment to the reveal moment.
- Cooldown alone (with the price still public at list time) does not
  help — searcher and buyer both wait, both race after.
- Together: the price is hidden until reveal, AND the buyer has a
  fixed window post-reveal to land first. The 30s cooldown is large
  enough for a UI-driven buyer to act and small enough that listings
  don't feel stale.

State changes
=============

EscapeValveListing (+40 bytes, 99 → 139):
  + commit_hash:    [u8; 32]
  + buyable_after:  i64

EscapeValveStatus enum:
  + Pending = 3   (committed but not yet revealed; not buyable)

ProtocolConfig (+1 byte from forward-compat pad):
  + commit_reveal_required: bool   (defaults to false; flipped via
    `update_protocol_config` post-canary)

New constants:
  + REVEAL_COOLDOWN_SECS: i64 = 30

New errors:
  + CommitRevealRequired      (legacy path rejected by gate)
  + ListingNotPending         (reveal called on non-Pending listing)
  + InvalidCommitHash         (price/salt mismatch on reveal)
  + ListingNotBuyableYet      (buy attempted during cooldown)

SDK updated (sdk/src/onchain-raw.ts):
  + LISTING_ACCOUNT_SIZE: 99 → 139
  + RawListingView.commitHash + buyableAfter fields
  + LocalListingStatus adds "pending"

Docs
====
  + docs/security/mev-front-running.md § 2.2 — mitigation status flipped
    from 🔵 pending → 🟡 mitigation #1 (commit-reveal) shipped on-chain;
    mitigation #4 (Jito bundling) is operator-side and recommended for
    mainnet rampup but not strictly required given the on-chain cooldown
    enforces a deterministic head-start. § 3 summary table updated to
    reflect the new bounded-by-cooldown posture; big-picture paragraph
    no longer flags `escape_valve_buy` as the single non-bounded vector.

Validation
==========
  $ cargo check -p roundfi-core    # green (35 warnings, all pre-
                                     existing anchor-debug cfg warnings)
  $ pnpm typecheck                  # green (workspace)
  $ pnpm lint                       # green
  $ pnpm test:parity                # 7 passing
  $ pnpm test:app-encoders          # 58 passing
  $ pnpm test:events                # 2 passing
  $ pnpm test:economic-parity-l1    # 45 passing

`anchor build` (SBF target) remains blocked by the mpl-core ↔ Anchor 1.0
borsh coexistence issue tracked in PR #319 — same posture as every
recent on-chain PR. Host-side `cargo check` validates the code shape;
runtime testing arrives when #319 unblocks.

Follow-ups (out of scope here)
==============================
- App-side TS encoders for `escape_valve_list_commit` /
  `escape_valve_list_reveal` (frontend wiring).
- Bankrun coverage of the new flow (commit + reveal + cooldown + buy)
  — gated on the bankrun harness restoration tracked under #319.
- Operator-side Jito bundling for reveal + buy (mainnet operational
  layer, not on-chain).

https://claude.ai/code/session_01YapZy1Z5gzbV5EammBkSQm
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
round_financial Ready Ready Preview, Comment May 15, 2026 10:29am

@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

Deployment failed with the following error:

Resource is limited - try again in 24 hours (more than 100, code: "api-deployments-free-per-day").

Learn More: https://vercel.com/alrimarleskovars-projects?upgradeToPro=build-rate-limit

@alrimarleskovar alrimarleskovar merged commit fbc931e into main May 15, 2026
5 of 6 checks passed
alrimarleskovar added a commit that referenced this pull request May 15, 2026
Doc-debt sweep: aligns MAINNET_READINESS.md, README.md, audit-readiness.md, mev-front-running.md, and mainnet-canary-plan.md with the codebase after the Kamino harvest path landed and closed #233.

- MAINNET_READINESS.md § 4.5 flipped 🟡⛔ → ✅; § 4.1 canary gate list dropped #233 and annotated #230 with PR #319 upstream-blocked status; § 7 critical path reworked to surface #319 as the live blocker.
- docs/security/audit-readiness.md "pitch vs shipped" row — both deposit + harvest paths now in scope.
- docs/security/mev-front-running.md § 2.4 — Kamino sandwich vector reframed as bounded (slippage guard + PrincipalLoss revert); summary table merged with #322's cooldown row.
- docs/operations/mainnet-canary-plan.md — yield branch unconditional (was gated on #233).
- README.md Yield Waterfall paragraph reflects both paths shipped.

No code changes.
alrimarleskovar pushed a commit that referenced this pull request May 17, 2026
- §Security & Audit severity table reconciled with internal-audit-findings.md (34/31 → 40/36; Critical 2→3, Low 10→12, Informational 6→9, won't-fix 2→3).
- §Development Status row 1: 195+ → 370+ PRs merged on main (496 commits since project start).
- §Development Status row 4: smart contract LoC 6,150 → 8,655 across the 3 in-scope Anchor programs (matches AUDIT_SCOPE.md).
- §Development Status row 5: test count 227 → 280+ across 22 spec files; added explicit breakdown (security/encoder/canary-control/audit-regression/lifecycle) + bankrun_compat shim mention (ADR 0007).
- §Development Status row 9 (security audit) rewritten: stale 'Halborn/Ottersec/Sec3 + bug bounty deferred — out of hackathon scope' framing replaced with current 'internal pre-audit complete, 40 findings, 36 closed, Adevar engagement in scoping' framing; added pointer to all 11 indexed security docs.
- §Development Status row 10 (devnet testing) tail: bankrun_compat shim (ADR 0007) + Squads multisig rotation rehearsal evidence (2026-05-16, devnet program-id 6WuSo1ut…7Rpn).
- §Development Status row 11 (mainnet migration): SEV PR range #326..#365#326..#372.
- §Core Mechanics line 107: '[Adevar SEV-025]' → 'internal pre-audit [SEV-025]' (matches the constraint enforced in wave 3).
- docs/status.md: §Recent additions title #322-#369#322-#372 to capture wave 3 tail.

Drift discovered after wave 3 by re-reading the README end-to-end against
the canonical sources (AUDIT_SCOPE.md, internal-audit-findings.md). The
prose surface had been refreshed at the top (§Security & Audit > stats)
but the dependent severity table and §Development Status rows were
written before the internal pre-audit completed and the wave 1-3 doc
refresh.

This is the same Adevar-attribution drift the wave 3 PR (#372) closed
across docs/security/, applied to the README sections that escaped
the wave 1-3 scope. Internal pre-audit framing preserved verbatim.

https://claude.ai/code/session_01YapZy1Z5gzbV5EammBkSQm
alrimarleskovar added a commit that referenced this pull request May 17, 2026
… sweep (#373)

- §Security & Audit severity table reconciled with internal-audit-findings.md (34/31 → 40/36; Critical 2→3, Low 10→12, Informational 6→9, won't-fix 2→3).
- §Development Status row 1: 195+ → 370+ PRs merged on main (496 commits since project start).
- §Development Status row 4: smart contract LoC 6,150 → 8,655 across the 3 in-scope Anchor programs (matches AUDIT_SCOPE.md).
- §Development Status row 5: test count 227 → 280+ across 22 spec files; added explicit breakdown (security/encoder/canary-control/audit-regression/lifecycle) + bankrun_compat shim mention (ADR 0007).
- §Development Status row 9 (security audit) rewritten: stale 'Halborn/Ottersec/Sec3 + bug bounty deferred — out of hackathon scope' framing replaced with current 'internal pre-audit complete, 40 findings, 36 closed, Adevar engagement in scoping' framing; added pointer to all 11 indexed security docs.
- §Development Status row 10 (devnet testing) tail: bankrun_compat shim (ADR 0007) + Squads multisig rotation rehearsal evidence (2026-05-16, devnet program-id 6WuSo1ut…7Rpn).
- §Development Status row 11 (mainnet migration): SEV PR range #326..#365#326..#372.
- §Core Mechanics line 107: '[Adevar SEV-025]' → 'internal pre-audit [SEV-025]' (matches the constraint enforced in wave 3).
- docs/status.md: §Recent additions title #322-#369#322-#372 to capture wave 3 tail.

Drift discovered after wave 3 by re-reading the README end-to-end against
the canonical sources (AUDIT_SCOPE.md, internal-audit-findings.md). The
prose surface had been refreshed at the top (§Security & Audit > stats)
but the dependent severity table and §Development Status rows were
written before the internal pre-audit completed and the wave 1-3 doc
refresh.

This is the same Adevar-attribution drift the wave 3 PR (#372) closed
across docs/security/, applied to the README sections that escaped
the wave 1-3 scope. Internal pre-audit framing preserved verbatim.

https://claude.ai/code/session_01YapZy1Z5gzbV5EammBkSQm

Co-authored-by: Claude <noreply@anthropic.com>
@alrimarleskovar alrimarleskovar deleted the feat/escape-valve-commit-reveal-232 branch May 17, 2026 09:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants