From 83406ad405ca7ada5bac5beb525d5d1245dd7936 Mon Sep 17 00:00:00 2001 From: Ethan Frey Date: Tue, 15 Dec 2020 22:52:44 +0100 Subject: [PATCH] Properly handle missing quorum and hitting threshold edge case --- contracts/cw3-flex-multisig/src/contract.rs | 57 +++++++++++++++++ contracts/cw3-flex-multisig/src/state.rs | 71 +++++++++++++++++++-- 2 files changed, 121 insertions(+), 7 deletions(-) diff --git a/contracts/cw3-flex-multisig/src/contract.rs b/contracts/cw3-flex-multisig/src/contract.rs index 0f914349d..016c99912 100644 --- a/contracts/cw3-flex-multisig/src/contract.rs +++ b/contracts/cw3-flex-multisig/src/contract.rs @@ -1536,4 +1536,61 @@ mod tests { app.update_block(expire(voting_period)); assert_eq!(prop_status(&app), Status::Passed); } + + #[test] + fn quorum_enforced_even_if_absolute_threshold_met() { + let mut app = mock_app(); + + // 33% required for quora, which is 5 of the initial 15 + // 50% yes required to pass early (8 of the initial 15) + let voting_period = Duration::Time(20000); + let (flex_addr, _) = setup_test_case( + &mut app, + // note that 60% yes is not enough to pass without 20% no as well + Threshold::ThresholdQuora { + threshold: Decimal::percent(60), + quorum: Decimal::percent(80), + }, + voting_period, + coins(10, "BTC"), + false, + ); + + // create proposal + let proposal = pay_somebody_proposal(&flex_addr); + let res = app + .execute_contract(VOTER5, &flex_addr, &proposal, &[]) + .unwrap(); + // Get the proposal id from the logs + let proposal_id: u64 = res.attributes[2].value.parse().unwrap(); + let prop_status = |app: &App| -> Status { + let query_prop = QueryMsg::Proposal { proposal_id }; + let prop: ProposalResponse = app + .wrap() + .query_wasm_smart(&flex_addr, &query_prop) + .unwrap(); + prop.status + }; + assert_eq!(prop_status(&app), Status::Open); + app.update_block(|block| block.height += 3); + + // reach 60% of yes votes, not enough to pass early (or late) + let yes_vote = HandleMsg::Vote { + proposal_id, + vote: Vote::Yes, + }; + app.execute_contract(VOTER4, &flex_addr, &yes_vote, &[]) + .unwrap(); + // 9 of 15 is 60% absolute threshold, but less than 12 (80% quorum needed) + assert_eq!(prop_status(&app), Status::Open); + + // add 3 weight no vote and we hit quorum and this passes + let no_vote = HandleMsg::Vote { + proposal_id, + vote: Vote::No, + }; + app.execute_contract(VOTER3, &flex_addr, &no_vote, &[]) + .unwrap(); + assert_eq!(prop_status(&app), Status::Passed); + } } diff --git a/contracts/cw3-flex-multisig/src/state.rs b/contracts/cw3-flex-multisig/src/state.rs index 9fde918f7..9ab664af2 100644 --- a/contracts/cw3-flex-multisig/src/state.rs +++ b/contracts/cw3-flex-multisig/src/state.rs @@ -103,23 +103,22 @@ impl Proposal { Threshold::AbsolutePercentage { percentage: percentage_needed, } => self.votes.yes >= votes_needed(self.total_weight, percentage_needed), - Threshold::ThresholdQuora { - threshold, - quorum: quroum, - } => { + Threshold::ThresholdQuora { threshold, quorum } => { // this one is tricky, as we have two compares: if self.expires.is_expired(block) { // * if we have closed yet, we need quorum% of total votes to have voted (counting abstain) // and threshold% of yes votes from those who voted (ignoring abstain) let total = self.votes.total(); let opinions = total - self.votes.abstain; - total >= votes_needed(self.total_weight, quroum) + total >= votes_needed(self.total_weight, quorum) && self.votes.yes >= votes_needed(opinions, threshold) } else { // * if we have not closed yet, we need threshold% of yes votes (from 100% voters - abstain) // as we are sure this cannot change with any possible sequence of future votes - self.votes.yes - >= votes_needed(self.total_weight - self.votes.abstain, threshold) + // * we also need quorum (which may not always be the case above) + self.votes.total() >= votes_needed(self.total_weight, quorum) + && self.votes.yes + >= votes_needed(self.total_weight - self.votes.abstain, threshold) } } } @@ -363,4 +362,62 @@ mod test { check_is_passed(quorum.clone(), passing.clone(), 16, false) ); } + + #[test] + fn quorum_edge_cases() { + // when we pass absolute threshold (everyone else voting no, we pass), but still don't hit quorum + let quorum = Threshold::ThresholdQuora { + threshold: Decimal::percent(60), + quorum: Decimal::percent(80), + }; + + // try 9 yes, 1 no (out of 15) -> 90% voter threshold, 60% absolute threshold, still no quorum + // doesn't matter if expired or not + let missing_voters = Votes { + yes: 9, + no: 1, + abstain: 0, + veto: 0, + }; + assert_eq!( + false, + check_is_passed(quorum.clone(), missing_voters.clone(), 15, false) + ); + assert_eq!( + false, + check_is_passed(quorum.clone(), missing_voters.clone(), 15, true) + ); + + // 1 less yes, 3 vetos and this passes only when expired + let wait_til_expired = Votes { + yes: 8, + no: 1, + abstain: 0, + veto: 3, + }; + assert_eq!( + false, + check_is_passed(quorum.clone(), wait_til_expired.clone(), 15, false) + ); + assert_eq!( + true, + check_is_passed(quorum.clone(), wait_til_expired.clone(), 15, true) + ); + + // 9 yes and 3 nos passes early + let passes_early = Votes { + yes: 9, + no: 3, + abstain: 0, + veto: 0, + }; + assert_eq!( + true, + check_is_passed(quorum.clone(), passes_early.clone(), 15, false) + ); + assert_eq!( + true, + check_is_passed(quorum.clone(), passes_early.clone(), 15, true) + ); + } }