feat(dispute): enforce bond slashing, winner refund, and reputation deltas on resolution - #1787
Merged
Merged
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Summary
Closes the economic and reputational griefing hole where a losing disputer could
file a bond-backed challenge with zero consequences. Every settlement path now
slashes the loser exactly once, makes the winner whole, and records a reputation
delta for both parties.
Problem
dispute.rsheld the disputer's bond in escrow but the rejection path (uphold = false) calledescrow::slash_funds— which only credited the protocol treasury —without ever returning anything to the winner or penalising the loser's reputation.
An upheld dispute returned the disputer's bond but did not penalise the market
creator for having their resolution overturned. Neither path was guarded against
being called twice on the same dispute record.
Concretely:
reputation penalty beyond the one already applied when the dispute was raised.
resolve_disputeandfinalize_arbiter_votecould be called a second time ona dispute that had already been settled, risking double-refund or double-slash.
resolve_appealcould be called again after the appeal bond was cleared,silently doing nothing rather than reverting.
Changes
contracts/open-market/src/storage_types.rsis_resolvedboolresolution_upheldOption<bool>contracts/open-market/src/errors.rsThe
#[contracterror]enum is hard-capped at 50 XDR cases and was already atthat limit. New error semantics are carried by existing variants, documented in
code:
DisputeAlreadyResolvedZeroShareTransferNothingToSlashEscrowEmptycontracts/open-market/src/escrow.rsAdded
distribute_slashed_bond(env, winner, winner_refund, total_bond):checked_sub,checked_mul,checked_div).InvalidInputifwinner_refund > total_bond.refund()path (reentrancy-guarded).contracts/open-market/src/reputation.rsTwo new mutation hooks, consistent with the existing
on_dispute_raisedpattern:on_dispute_upheld(env, creator)— applied when the disputer wins;increments the market creator's
dispute_counta second time, compounding thepenalty already applied when the dispute was raised.
on_dispute_rejected(env, disputer)— applied when the disputer loses;increments the disputer's
dispute_count, reducing their reputation scoreaccording to the existing
calculate_creator_reputationformula(
min(dispute_count * 50, 200)penalty, capped at 200 pts).contracts/open-market/src/dispute.rsAll three settlement paths updated uniformly:
resolve_disputeZeroShareTransferifis_resolvedis_resolved = true,resolution_upheld = Some(uphold)and persist — before any fund movementuphold = true→distribute_slashed_bond(winner = disputer, refund = bond, total = bond)+ reopen market +on_dispute_upheld(creator)uphold = false→distribute_slashed_bond(winner = None, refund = 0, total = bond)+on_dispute_rejected(disputer)resolve_appealappeal_bond→ returnEscrowEmptyifappeal_bond == 0(double-call guard)uphold = true→ full refund to appealer + reopen market +on_dispute_upheld(creator)uphold = false→ full slash +on_dispute_rejected(appealer)finalize_arbiter_voteZeroShareTransferifis_resolvedis_resolved = trueand persist — before slash-and-redistributeresolve_disputewith reputation deltasTests
contracts/open-market/tests/dispute_tests.rs— 10 new tests, all passing:test_resolve_dispute_reject_slashes_bond_correctlytest_resolve_dispute_uphold_refunds_disputer_full_bondtest_resolve_dispute_cannot_be_resolved_twiceDisputeNotFoundtest_resolve_dispute_reject_penalizes_disputer_reputationdispute_countincrementstest_resolve_dispute_uphold_penalizes_creator_reputationdispute_count(raise + resolution) and score is bounded by the penalty formulatest_resolve_appeal_uphold_refunds_appealer_full_bondtest_resolve_appeal_reject_slashes_appealer_bondtest_resolve_appeal_cannot_be_resolved_twiceEscrowEmptytest_complete_dispute_lifecycle_with_reputation_trackingtest_uphold_dispute_reopens_market_and_updates_all_partiestest_checked_arithmetic_in_bond_distributionbondwith no rounding lossAll pre-existing dispute tests continue to pass (36/36). Reputation test suite
also green (7/7).
Acceptance criteria
distribute_slashed_bondrefunds full bond to winneris_resolvedflag blocks second settlementon_dispute_upheld/on_dispute_rejectedhooksZeroShareTransfer/EscrowEmpty/DisputeNotFoundchecked_sub,checked_mul,checked_divindistribute_slashed_bondNotes for reviewers
DisputeAlreadyResolvedandNothingToSlashare newsemantic concepts but reuse existing discriminants (
112and32) because the50-case XDR cap is already exhausted. The reuse is documented in
errors.rsandboth call sites. If the cap is ever raised in a future SDK version these should
be extracted into proper variants.
distribute_slashed_bondispub(crate)— it is not exposed in thecontract's public ABI; it is an internal escrow primitive used exclusively by
the three dispute settlement paths.
min(dispute_count * 50, 200)) and the insurance poolsplit (
insurance_pool_share_bps, default 10 %) are both governance-configurable— no magic numbers were introduced.
closes #1761