Getting Started
- Fork the repository: https://github.com/JointSave-org/Joint_Save
- Clone your fork:
git clone https://github.com/<your-username>/Joint_Save.git
cd Joint_Save
- Create a new branch:
git checkout -b feat/dispute-resolution
Overview
When a pool member misses a deposit or disputes a penalty applied by the admin, there is currently no formal mechanism to handle the conflict. The admin has unilateral power to remove members and apply penalties, with no appeals process. This creates trust issues, especially in larger pools where members may not know each other personally.
Checked frontend/app/dashboard/group/[id]/GroupClient.tsx — the admin actions (emergency_withdraw, pause, remove_member) are directly callable. There is no dispute table in Supabase. The pool_activity table records actions but has no dispute/resolution concept.
This issue adds a dispute resolution system where members can file disputes against admin actions or other members, other members can vote to resolve the dispute, and resolutions are recorded on-chain and off-chain.
Requirements
Database Changes
-
New table disputes:
id (uuid, PK, default gen_random_uuid())
pool_id (text, not null)
filer_address (text, not null) — the member filing the dispute
target_address (text, nullable) — the member/admin being disputed against (null for admin action disputes)
dispute_type (text, not null): missed_deposit, unfair_penalty, admin_abuse, member_misconduct, other
description (text, not null, max 2000 chars)
evidence_urls (jsonb, default '[]') — optional links to evidence
status (text, not null, default 'open'): open, voting, resolved_upheld, resolved_dismissed, expired
resolution (text, nullable) — explanation of the resolution
votes_for (integer, default 0)
votes_against (integer, default 0)
resolved_by (text, nullable) — address of the resolving voter
resolved_at (timestamptz, nullable)
created_at (timestamptz, default now())
expires_at (timestamptz, not null) — 72 hours from creation
- RLS: pool members can read disputes for their pools; filer can insert/update their own; authenticated users can update vote counts
-
New table dispute_votes:
dispute_id (uuid, FK to disputes)
voter_address (text)
vote (boolean) — true = uphold, false = dismiss
created_at (timestamptz)
- PK: (dispute_id, voter_address)
- RLS: pool members can read; authenticated users can insert their own vote
API Changes
POST /api/disputes — file a new dispute (body: pool_id, target_address, dispute_type, description, evidence_urls)
- Validates filer is a member of the pool
- Validates no active dispute already exists for the same type+target in the same pool
- Creates dispute with 72-hour expiry
GET /api/disputes?pool_id=X — list disputes for a pool, sorted by created_at DESC
POST /api/disputes/[id]/vote — vote on a dispute (body: vote: boolean)
- Validates voter is a member, not the filer, not the target, hasn't already voted
- Increments votes_for or votes_against
- If votes_for >= 50% of pool members: auto-resolve as
resolved_upheld
- If votes_against >= 50%: auto-resolve as
resolved_dismissed
PUT /api/disputes/[id]/resolve — admin manually resolves (body: resolution: string, outcome: 'upheld'|'dismissed')
- Only pool admin can call
- Required when voting ends in a tie
POST /api/disputes/expire — cron endpoint to expire disputes past their expires_at
Frontend Changes
- New tab in group detail page: "Disputes" (alongside Members, Activity, Chat)
- Lists all disputes for the pool with status badges
- "File Dispute" button opens a dialog
- Each dispute card shows: filer, target, type, description, vote counts, time remaining, status
- New component:
frontend/components/disputes/file-dispute-dialog.tsx
- Dropdown for dispute type
- Text area for description (2000 char limit with counter)
- Optional evidence URL inputs (max 3)
- Submit button (disabled until required fields filled)
- New component:
frontend/components/disputes/dispute-card.tsx
- Shows dispute details with vote buttons
- "Upvote to Uphold" / "Downvote to Dismiss" buttons
- Visual progress bar showing vote split
- Time remaining countdown
- Resolution text when resolved
- New component:
frontend/components/disputes/dispute-resolution-banner.tsx
- Banner shown at top of group page when a dispute has been resolved
- Brief summary of the outcome
Activity Logging
- When a dispute is filed: insert into
pool_activity with activity_type: 'dispute_filed'
- When a dispute is resolved: insert with
activity_type: 'dispute_resolved' and resolution details in metadata
Acceptance Criteria
Getting Started
Overview
When a pool member misses a deposit or disputes a penalty applied by the admin, there is currently no formal mechanism to handle the conflict. The admin has unilateral power to remove members and apply penalties, with no appeals process. This creates trust issues, especially in larger pools where members may not know each other personally.
Checked
frontend/app/dashboard/group/[id]/GroupClient.tsx— the admin actions (emergency_withdraw, pause, remove_member) are directly callable. There is no dispute table in Supabase. Thepool_activitytable records actions but has no dispute/resolution concept.This issue adds a dispute resolution system where members can file disputes against admin actions or other members, other members can vote to resolve the dispute, and resolutions are recorded on-chain and off-chain.
Requirements
Database Changes
New table
disputes:id(uuid, PK, default gen_random_uuid())pool_id(text, not null)filer_address(text, not null) — the member filing the disputetarget_address(text, nullable) — the member/admin being disputed against (null for admin action disputes)dispute_type(text, not null):missed_deposit,unfair_penalty,admin_abuse,member_misconduct,otherdescription(text, not null, max 2000 chars)evidence_urls(jsonb, default '[]') — optional links to evidencestatus(text, not null, default 'open'):open,voting,resolved_upheld,resolved_dismissed,expiredresolution(text, nullable) — explanation of the resolutionvotes_for(integer, default 0)votes_against(integer, default 0)resolved_by(text, nullable) — address of the resolving voterresolved_at(timestamptz, nullable)created_at(timestamptz, default now())expires_at(timestamptz, not null) — 72 hours from creationNew table
dispute_votes:dispute_id(uuid, FK to disputes)voter_address(text)vote(boolean) — true = uphold, false = dismisscreated_at(timestamptz)API Changes
POST /api/disputes— file a new dispute (body: pool_id, target_address, dispute_type, description, evidence_urls)GET /api/disputes?pool_id=X— list disputes for a pool, sorted by created_at DESCPOST /api/disputes/[id]/vote— vote on a dispute (body: vote: boolean)resolved_upheldresolved_dismissedPUT /api/disputes/[id]/resolve— admin manually resolves (body: resolution: string, outcome: 'upheld'|'dismissed')POST /api/disputes/expire— cron endpoint to expire disputes past theirexpires_atFrontend Changes
frontend/components/disputes/file-dispute-dialog.tsxfrontend/components/disputes/dispute-card.tsxfrontend/components/disputes/dispute-resolution-banner.tsxActivity Logging
pool_activitywithactivity_type: 'dispute_filed'activity_type: 'dispute_resolved'and resolution details in metadataAcceptance Criteria