@@ -88,6 +88,50 @@ fn push_status_transition(
8888 }
8989}
9090
91+ // ── Participation quorum (see also `types` lifecycle docs) ───────────────────
92+ //
93+ // Let `E` = eligible voters (snapshot length at `file_claim`), `C` = cast ballots =
94+ // `approve_votes + reject_votes`, `Q` = quorum basis points **for this claim**
95+ // (instance `quorum_bps` copied into persistent `ClaimQuorumBps(claim_id)` at filing).
96+ // Admin changes to instance `quorum_bps` do **not** alter `Q` for claims already in
97+ // `Processing`.
98+ //
99+ // Required minimum cast votes:
100+ // R = ceil(E * Q / 10_000) → R = (E * Q + 9_999) / 10_000 (u32; E = 0 ⇒ R = 0)
101+ //
102+ // **Quorum met** iff `C >= R`. If met, outcome is **plurality**: Approved when
103+ // `approve_votes > reject_votes`, else Rejected (insurer wins ties).
104+ // If the voting deadline passes with `C < R`, the claim is **Rejected** (no quorum).
105+ fn required_cast_for_quorum ( eligible : u32 , quorum_bps : u32 ) -> u32 {
106+ if eligible == 0 {
107+ return 0 ;
108+ }
109+ let numer = ( eligible as u64 ) . saturating_mul ( quorum_bps as u64 ) ;
110+ numer. div_ceil ( 10_000 ) as u32
111+ }
112+
113+ fn participation_quorum_met ( cast_votes : u32 , eligible : u32 , quorum_bps : u32 ) -> bool {
114+ cast_votes >= required_cast_for_quorum ( eligible, quorum_bps)
115+ }
116+
117+ /// If participation quorum is satisfied, returns Some(Approved|Rejected) by plurality.
118+ fn resolve_plurality_if_quorum_met (
119+ approve_votes : u32 ,
120+ reject_votes : u32 ,
121+ cast_votes : u32 ,
122+ eligible : u32 ,
123+ quorum_bps : u32 ,
124+ ) -> Option < ClaimStatus > {
125+ if !participation_quorum_met ( cast_votes, eligible, quorum_bps) {
126+ return None ;
127+ }
128+ if approve_votes > reject_votes {
129+ Some ( ClaimStatus :: Approved )
130+ } else {
131+ Some ( ClaimStatus :: Rejected )
132+ }
133+ }
134+
91135// ── Events ────────────────────────────────────────────────────────────────────
92136
93137#[ contractevent( topics = [ "niffyinsure" , "claim_filed" ] ) ]
@@ -259,6 +303,7 @@ pub fn file_claim(
259303 storage:: set_claim ( env, & claim) ;
260304 storage:: set_open_claim ( env, holder, policy_id, true ) ;
261305 storage:: snapshot_claim_voters ( env, claim_id) ;
306+ storage:: set_claim_quorum_bps ( env, claim_id, storage:: get_quorum_bps ( env) ) ;
262307 storage:: set_last_claim_ledger ( env, holder, now) ;
263308
264309 let mut evidence_hashes: Vec < BytesN < 32 > > = Vec :: new ( env) ;
@@ -331,14 +376,21 @@ pub fn vote_on_claim(
331376 VoteOption :: Reject => claim. reject_votes += 1 ,
332377 }
333378
334- // Auto-finalize on majority.
335- let total = snapshot. len ( ) ;
336- let majority = total / 2 + 1 ;
337- if claim. approve_votes >= majority {
338- claim. status = ClaimStatus :: Approved ;
339- } else if claim. reject_votes >= majority {
340- claim. status = ClaimStatus :: Rejected ;
341- claim. appeal_open_deadline_ledger = now. saturating_add ( ledger:: APPEAL_OPEN_WINDOW_LEDGERS ) ;
379+ let eligible = snapshot. len ( ) as u32 ;
380+ let cast = claim. approve_votes + claim. reject_votes ;
381+ let quorum_bps = storage:: get_claim_quorum_bps ( env, claim_id) ;
382+ if let Some ( res) = resolve_plurality_if_quorum_met (
383+ claim. approve_votes ,
384+ claim. reject_votes ,
385+ cast,
386+ eligible,
387+ quorum_bps,
388+ ) {
389+ let rejected = res == ClaimStatus :: Rejected ;
390+ claim. status = res;
391+ if rejected {
392+ claim. appeal_open_deadline_ledger = now. saturating_add ( ledger:: APPEAL_OPEN_WINDOW_LEDGERS ) ;
393+ }
342394 }
343395
344396 if claim. status != status_before {
@@ -380,7 +432,8 @@ pub fn refresh_snapshot(env: &Env, claim_id: u64) -> Result<(), Error> {
380432/// Finalize a claim after the voting deadline has passed.
381433///
382434/// Window check: `now > claim.voting_deadline_ledger` (see `ledger::is_claim_past_voting_deadline`).
383- /// Plurality wins; tie resolves to Rejected.
435+ /// Uses the **participation quorum** and per-claim `quorum_bps` snapshot (see module helpers).
436+ /// If quorum is met, plurality decides; if not, **Rejected** (no quorum).
384437pub fn finalize_claim ( env : & Env , claim_id : u64 ) -> Result < ClaimStatus , Error > {
385438 // Check pause: finalization is blocked if claims_paused is true
386439 storage:: assert_claims_not_paused ( env) ;
@@ -398,10 +451,20 @@ pub fn finalize_claim(env: &Env, claim_id: u64) -> Result<ClaimStatus, Error> {
398451
399452 let status_before = claim. status . clone ( ) ;
400453
401- if claim. approve_votes > claim. reject_votes {
402- claim. status = ClaimStatus :: Approved ;
454+ let voters = storage:: get_claim_voters ( env, claim_id) ;
455+ let eligible = voters. len ( ) as u32 ;
456+ let cast = claim. approve_votes + claim. reject_votes ;
457+ let quorum_bps = storage:: get_claim_quorum_bps ( env, claim_id) ;
458+
459+ if participation_quorum_met ( cast, eligible, quorum_bps) {
460+ if claim. approve_votes > claim. reject_votes {
461+ claim. status = ClaimStatus :: Approved ;
462+ } else {
463+ claim. status = ClaimStatus :: Rejected ;
464+ claim. appeal_open_deadline_ledger = now. saturating_add ( ledger:: APPEAL_OPEN_WINDOW_LEDGERS ) ;
465+ }
403466 } else {
404- // Tie or reject plurality → Rejected (insurer wins tie ).
467+ // Below minimum participation — no quorum (insurer-favored default ).
405468 claim. status = ClaimStatus :: Rejected ;
406469 claim. appeal_open_deadline_ledger = now. saturating_add ( ledger:: APPEAL_OPEN_WINDOW_LEDGERS ) ;
407470 }
0 commit comments