Skip to content

Campaign Dispute Resolution System #674

Description

@grantfox-oss

Overview

CrowdPay's multisig escrow model prevents unilateral withdrawals, but it provides no mechanism for dispute resolution when a creator and contributor disagree. A contributor who believes a campaign is fraudulent has no recourse — they cannot reclaim funds, and the platform has no structured process for handling the dispute. This issue implements a complete dispute resolution workflow: a contributor can raise a dispute, the platform freezes the campaign escrow by adding a third signer and raising the threshold to 3, evidence is submitted by both parties, a platform arbitrator reviews and issues a decision, and the escrow is either released to the creator or refunded to contributors.

What needs to be built

backend/src/services/dispute.js — Dispute engine

  • POST /api/campaigns/:id/disputes — contributor-initiated:

    • Validates: contributor has made at least one confirmed contribution to this campaign
    • Creates a disputes record with status: open
    • Triggers the escrow freeze: builds and submits a SetOptions transaction that adds the platform arbitrator key as a third signer (weight 1) and raises the medium threshold from 2 to 3
    • Sends email notifications to the creator and the platform ops team
    • Returns { disputeId, status: 'open', frozenAt }
  • POST /api/disputes/:id/evidence — either party (creator or contributor) can submit:

    • Accepts { text, attachmentUrls: string[] } — text description + links to external evidence
    • Creates dispute_evidence record with submitted_by and role: 'creator' | 'contributor'
  • POST /api/admin/disputes/:id/decide — platform arbitrator only:

    • Accepts { decision: 'release_to_creator' | 'refund_contributors', reason: string }
    • release_to_creator: removes the arbitrator signer and lowers the threshold back to 2; creator can withdraw normally
    • refund_contributors: builds a transaction that sends the campaign balance back to all contributors proportionally; requires creator + platform signatures
    • Updates dispute status to resolved; emails both parties with the decision and reason
      Frontend — Campaign page additions
  • "Raise Dispute" button — visible to contributors only after the campaign deadline has passed and no withdrawal has been initiated; requires confirmation modal

  • Dispute status banner on the campaign page — visible to all once a dispute is open

  • Evidence submission form (in the dispute detail page): text area + "Add URL" inputs

  • Admin dispute queue (/admin/disputes): list of open disputes with campaign name, dispute age, evidence count; "Review" button per dispute
    Database migrations

  • disputes: id, campaign_id, raised_by, status (open | under_review | resolved), decision, reason, arbitrator_signer_added, created_at, resolved_at

  • dispute_evidence: id, dispute_id, submitted_by, role, text, attachment_urls (jsonb), submitted_at

Acceptance criteria

  • Raising a dispute successfully adds the arbitrator key as a third signer and raises the threshold to 3 on the Stellar campaign wallet — confirmed by fetching the account from Horizon and inspecting signers
  • A contributor who has not made a confirmed contribution cannot raise a dispute — returns 403 NOT_A_CONTRIBUTOR
  • refund_contributors decision correctly computes each contributor's proportional refund amount — sum of all refund amounts equals the campaign balance
  • After release_to_creator decision, the arbitrator signer is removed and the threshold returns to 2 — creator can withdraw without the arbitrator's involvement
  • A campaign with an active dispute cannot accept new contributions — POST /api/contributions returns 409 CAMPAIGN_DISPUTED
  • Both parties receive email notifications within 60 seconds of a dispute being raised and when a decision is issued

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions