feat(roundfi-core): commit-reveal escape-valve listings (#232)#322
Merged
Conversation
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
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Deployment failed with the following error: Learn More: https://vercel.com/alrimarleskovars-projects?upgradeToPro=build-rate-limit |
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.
4 tasks
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
escape_valve_list_commit(commit_hash)EscapeValveListinginPendingstatus. OnlySHA-256(price ‖ salt)is on chain — searchers can't derive the price. Same eligibility constraints as legacyescape_valve_list.escape_valve_list_reveal(price, salt)(price, salt)againstcommit_hash, transitionsPending → Active, setsbuyable_after = now + REVEAL_COOLDOWN_SECS(30s). Authority: onlylisting.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 byconfig.commit_reveal_required. When the flag is on, returnsCommitRevealRequired; when off, setscommit_hash = [0;32]+buyable_after = listed_at(preserves devnet/demo single-step UX).update_protocol_config— newOption<bool>field fornew_commit_reveal_required.State additions
Constants
Errors
SDK (
sdk/src/onchain-raw.ts)LISTING_ACCOUNT_SIZE: 99 → 139RawListingViewgainscommitHash: Buffer+buyableAfter: bigintLocalListingStatusadds"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_buyno 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
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-sidecargo checkvalidates the code shape; runtime testing via bankrun arrives when #319 unblocks.Follow-ups (deliberately out of scope)
escape_valve_list_commit/escape_valve_list_revealMAINNET_READINESS.md§ 5.4 status flip (🟡 → ✅)Test plan
cargo checkgreentypecheck+lintgreenGenerated by Claude Code