Skip to content

fix(disputes): disputeFinalizesAt view, and an answer is one non-zero hash before the challenge deadline (D4, D5) - #53

Merged
JoE11-y merged 4 commits into
mainfrom
fix/dispute-finalize-view-answer-rules
Oct 5, 2026
Merged

JoE11-y merged 4 commits into
mainfrom
fix/dispute-finalize-view-answer-rules

Conversation

@JoE11-y

@JoE11-y JoE11-y commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Dispute fixes D4 and D5 from the relayer API audit, on both escrow implementations (EVM and Soroban). The relayer side is in a stacked monorepo PR.

D4: the relayer couldn't know when a dispute can be finalized. finalizeDispute refuses until the challenge deadline has passed and, when the co-signed payout was denied (a maker halt, or a key kill during the order) and the ruling isn't MakerForfeit, until the evidence grace has passed too. On EVM two inputs to "denied" (the lock time and the registry epoch) sit in an internal mapping with no view, so the relayer showed the challenge deadline and a finalize sent then was refused TooEarly.

  • New view disputeFinalizesAt(params) (EVM) / dispute_finalizes_at (Soroban), the dispute twin of cancelFinalizesAt: 0 unless disputed, else the effective challenge deadline, plus the grace when the payout was denied and the ruling isn't MakerForfeit.
  • It can't drift from the real check: on EVM finalizeCancel, finalizeDispute and both views read one private _finalizesAt; on Soroban finalize_dispute and the view read one dispute_end.
  • Tests on both chains: at view - 1 finalize reverts, at the view it succeeds; not denied, halted, key kill in the window, MakerForfeit, a pause, not disputed.

D5: answers to a dispute had no rules. recordResponse overwrote a single slot and accepted an answer at any time while disputed, including a zero hash. Now, before the write:

Rule EVM Soroban (module / ad-manager relay)
Empty answer refused DisputeManager__ZeroResponse() 27 / 84
Second answer refused DisputeManager__AlreadyResponded(orderHash) 26 / 83
Answer at or after the effective challenge deadline refused DisputeManager__ResponseWindowClosed(until) 25 / 82
Answer after the arbiter has ruled refused (review R1) DisputeManager__AlreadyRuled(orderHash) 23 / 85
  • The deadline is the arbiter's own cutoff, and the ruling closes the window: an answer can land until the arbiter rules or the deadline passes, whichever is first. (The first push accepted answers until the ruling's finalize time, because the ruling stores that time in the deadline slot; nothing designed that, and the relayer contract in the monorepo PR already says "the last second to rule or answer".) Whether a ruling can be re-ruled is #454, tranche 3, and is not touched here.
  • Interface change: Soroban record_response now takes the escrow's paused seconds, as outcome_of already does (the module can't read back into its caller).

How this was done: reproduced first on the current code, on both chains: a denied dispute finalized at the challenge deadline was refused TooEarly(90001) (EVM) / TooEarly (Soroban), and a second, a late and a zero answer were all accepted. Each fix was reverted once to confirm its test goes red.

Bytecode: the view alone put AdManager 231 bytes over the 24,576-byte limit. Two behaviour-neutral refactors (one helper for the outcomeOf read, one for the ads[params.adId] lookup) bring it to 24,441, 135 bytes under (it was 53 under before). DisputeManager 7,979 → 8,095. No circuit change, so no Verifier.sol regen.

Checks: forge fmt --check clean; forge 1,983 passed (incl. the ffi proof tests against circuits a1ee0f0), invariants 24, gas gates and OrderHashParity pass, error-coverage names all 144 errors. Soroban cargo fmt --check clean, unit tests green, all WASMs rebuilt, integration 242/242 incl. metering, parity steps pass. Local forge is 1.5.1; CI pins 1.7.1.

Review pass 1 (2026-10-05), on the branch rebased onto main 4c28171 (ProofBridgeUtils in)

  • R1 (user decision): the ruling closes the answer window, both chains, AlreadyRuled (row above). Reproduced first: with the guard removed the new test lands an answer after a MutualRefund ruling on EVM and on Soroban. The module's AlreadyRuled is now relayed 1:1 by both escrows (ad-manager 85, order-portal 105) instead of collapsing to DisputeModuleRejected; the core relay table and the module's code-table test carry it.
  • R2: Soroban cancel_finalizes_at and finalize_cancel read one cancel_end, the dispute_end shape; outcome_of uses deadline_at instead of its inline copy of the pause arithmetic.
  • R3: finalizeDispute passes the module and outcome it already read to _disputeEnd; one external read, not three. (Bytecode: AdManager 23,880, 696 spare on the rebased branch.)
  • R4: _finalizesExactlyAt pins the refusal at at − 1: TooEarly(at) inside the grace, DisputeNotResolved(orderHash) when the view equals the challenge deadline. Typing it surfaced that second shape, which the untyped expectRevert had hidden.
  • R5: both views' natspec: paused seconds accrue at unpause, re-read after one.
  • R6: the order-portal relays the three answer faults 1:1 (102–104), unreachable today, like every other module refusal.

For the monorepo PR (#548): one more rule row (AlreadyRuled, Soroban 85) in the D5 docs and the frontend's Soroban error table; the order-portal codes 102–105 are unreachable and need no row.

Checks (this pass): EVM 2,048 passed, non-invariant, forge fmt clean; Soroban unit suites core 22 / dispute-manager 43 / ad-manager 56 / order-portal 26, cargo fmt clean; integration suite 243 passed on freshly built WASM (dispute-manager, ad-manager, order-portal rebuilt per package).

@coderabbitai

coderabbitai Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3276b0ee-62aa-4259-82d7-7fe3d6462e0e
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…e at the effective challenge deadline is refused with the grace still to run (passes: the evidence), and a second answer, an answer at the challenge deadline and a zero answer are all accepted (red on the current code)
…he effective challenge deadline (the ruling's own cutoff): DisputeManager__ZeroResponse, __AlreadyResponded(orderHash), __ResponseWindowClosed(until) on EVM; ZeroResponse 27, AlreadyResponded 26, ResponseWindowClosed 25 on Soroban, relayed by the ad-manager as 84, 83, 82; Soroban record_response takes the escrow's paused seconds (the module cannot read back into its caller)
…lFinalizesAt's dispute twin: 0 off Disputed, else the effective challenge deadline plus the evidence grace when the co-signed payout was denied and the ruling is not MakerForfeit. It is the doors' own clock: on EVM finalizeCancel, finalizeDispute and both views read one _finalizesAt; on Soroban finalize_dispute and the view read one dispute_end. AdManager pays for the view by sharing the outcomeOf read and the ads[params.adId] lookup (EIP-170 margin 53 -> 135 bytes)
@JoE11-y
JoE11-y force-pushed the fix/dispute-finalize-view-answer-rules branch from 38a54fc to 229e7d0 Compare October 5, 2026 13:48
…(R1–R6)

R1 (user decision): an answer after the arbiter has ruled is refused, AlreadyRuled, on both chains.
The ruling stores its finalize time in the deadline slot, so without this an answer landed until
then, of use only to a re-ruling (#454, tranche 3, untouched). Reproduced first on both chains.
The module's AlreadyRuled is relayed 1:1 by both escrows now (ad-manager 85, order-portal 105)
instead of collapsing to DisputeModuleRejected; core relay + the module's code-table test carry it.

R2: Soroban cancel_finalizes_at and finalize_cancel read one cancel_end (the dispute_end shape);
outcome_of uses deadline_at. R3: finalizeDispute passes the module and outcome it already read
to _disputeEnd — one external read, not three. R4: _finalizesExactlyAt pins the refusal at at−1:
TooEarly(at) inside the grace, DisputeNotResolved when the view equals the challenge deadline (the
untyped expectRevert had hidden that shape). R5: both views' natspec — paused seconds accrue at
unpause, re-read after one. R6: the order-portal relays the three answer faults 1:1 (102–104).

EVM 2,048 non-invariant passed, fmt clean, AdManager 23,880 (696 spare). Soroban unit suites
core 22 / dispute-manager 43 / ad-manager 56 / order-portal 26, fmt clean; integration 243 on
freshly built WASM.
@JoE11-y
JoE11-y merged commit 14cb35c into main Oct 5, 2026
11 checks passed
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.

1 participant