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/dao-governance-voting
Overview
JointSave currently has no mechanism for pool members to collectively decide on pool parameters (e.g., changing deposit amounts, extending deadlines, or modifying penalty rules). All administrative decisions are made unilaterally by the pool creator/admin. This creates a single point of failure and does not reflect how real-world savings groups operate — where decisions are made collectively.
The roadmap (Phase 3) lists "DAO Governance — Community-driven protocol improvements" but no foundation exists. The factory contract has no governance extensions. The frontend has no voting UI.
This issue implements a lightweight on-chain governance system that allows pool members to propose and vote on parameter changes, with execution only occurring when a configurable quorum is met.
Requirements
Smart Contract Changes
- New contract:
smartcontract/governance/
- Storage:
proposals: Map<Bytes, Proposal> where Proposal contains:
proposer: Address
proposal_type: ProposalType (enum: ChangeDepositAmount, ExtendDeadline, AddPenalty, RemovePenalty, ChangeQuorum, Custom)
parameters: Map<String, Bytes> (serialized proposal-specific params)
votes_for: Vec<Address>
votes_against: Vec<Address>
status: ProposalStatus (enum: Active, Passed, Executed, Expired, Rejected)
created_at: u64
expires_at: u64 (48 hours default)
execution_result: Option<Vec<u8>>
voting_quorum: u32 (percentage of total members, e.g., 51 means 51%)
pool_contract: Address (the pool this governance contract is attached to)
- Functions:
create_proposal(member, proposal_type, parameters) -> proposal_id — only pool members can create; max 3 active proposals per pool
vote(proposer, proposal_id, in_favor: bool) — one vote per member per proposal; cannot vote on own proposal
execute_proposal(executor, proposal_id) — only succeeds if votes_for >= quorum percentage of total members AND proposal is not expired; executes the parameter change on the target pool contract via CPI
get_proposal(proposal_id) -> Proposal
get_active_proposals(pool_contract) -> Vec<Proposal>
get_voting_quorum() -> u32
Pool Contract Integration
- Add a new function to all three pool contracts:
apply_governance_proposal(admin, proposal_type, parameters) — admin-only but designed to be called via CPI from the governance contract
- The governance contract stores the pool contract address and can invoke
apply_governance_proposal via env.invoke_contract()
- Existing pool parameters that become governable:
deposit_amount (flexible pools)
round_duration (rotational pools)
penalty_percentage
Frontend Changes
- New component:
frontend/components/governance/proposal-list.tsx
- Lists all active and recent proposals for a pool
- Shows proposal type, proposer, vote counts, time remaining
- Color-coded status badges (Active=blue, Passed=green, Executed=gray, Expired=orange, Rejected=red)
- New component:
frontend/components/governance/create-proposal-dialog.tsx
- Modal dialog accessible from group detail page (admin section or governance tab)
- Dropdown for proposal type
- Dynamic form fields based on selected type (e.g., "New Deposit Amount" shows amount input)
- Character limit on proposal description (500 chars)
- New component:
frontend/components/governance/vote-card.tsx
- Shows proposal details with "Vote For" and "Vote Against" buttons
- Visual progress bar showing votes toward quorum
- Disable vote buttons if user already voted or is the proposer
- "Execute" button visible only when quorum is met and user is admin
- New tab or section in group detail page: "Governance" tab (visible when governance contract is deployed for the pool)
- New hook:
frontend/hooks/useGovernance.ts
- Fetches proposals, vote status, and quorum info for a pool
createProposal(), vote(), executeProposal() mutations
- Realtime vote count updates via Supabase Realtime subscription on a new
governance_votes table
Database Changes
- New table
governance_votes (off-chain mirror for realtime):
proposal_id (text)
pool_id (text)
voter_address (text)
vote (boolean)
created_at (timestamptz)
- RLS: members of the pool can read; authenticated users can insert their own
Acceptance Criteria
Getting Started
Overview
JointSave currently has no mechanism for pool members to collectively decide on pool parameters (e.g., changing deposit amounts, extending deadlines, or modifying penalty rules). All administrative decisions are made unilaterally by the pool creator/admin. This creates a single point of failure and does not reflect how real-world savings groups operate — where decisions are made collectively.
The roadmap (Phase 3) lists "DAO Governance — Community-driven protocol improvements" but no foundation exists. The
factorycontract has no governance extensions. The frontend has no voting UI.This issue implements a lightweight on-chain governance system that allows pool members to propose and vote on parameter changes, with execution only occurring when a configurable quorum is met.
Requirements
Smart Contract Changes
smartcontract/governance/proposals: Map<Bytes, Proposal>whereProposalcontains:proposer: Addressproposal_type: ProposalType(enum:ChangeDepositAmount,ExtendDeadline,AddPenalty,RemovePenalty,ChangeQuorum,Custom)parameters: Map<String, Bytes>(serialized proposal-specific params)votes_for: Vec<Address>votes_against: Vec<Address>status: ProposalStatus(enum:Active,Passed,Executed,Expired,Rejected)created_at: u64expires_at: u64(48 hours default)execution_result: Option<Vec<u8>>voting_quorum: u32(percentage of total members, e.g., 51 means 51%)pool_contract: Address(the pool this governance contract is attached to)create_proposal(member, proposal_type, parameters) -> proposal_id— only pool members can create; max 3 active proposals per poolvote(proposer, proposal_id, in_favor: bool)— one vote per member per proposal; cannot vote on own proposalexecute_proposal(executor, proposal_id)— only succeeds if votes_for >= quorum percentage of total members AND proposal is not expired; executes the parameter change on the target pool contract via CPIget_proposal(proposal_id) -> Proposalget_active_proposals(pool_contract) -> Vec<Proposal>get_voting_quorum() -> u32Pool Contract Integration
apply_governance_proposal(admin, proposal_type, parameters)— admin-only but designed to be called via CPI from the governance contractapply_governance_proposalviaenv.invoke_contract()deposit_amount(flexible pools)round_duration(rotational pools)penalty_percentageFrontend Changes
frontend/components/governance/proposal-list.tsxfrontend/components/governance/create-proposal-dialog.tsxfrontend/components/governance/vote-card.tsxfrontend/hooks/useGovernance.tscreateProposal(),vote(),executeProposal()mutationsgovernance_votestableDatabase Changes
governance_votes(off-chain mirror for realtime):proposal_id(text)pool_id(text)voter_address(text)vote(boolean)created_at(timestamptz)Acceptance Criteria