Skip to content

Commit

Permalink
Properly handle missing quorum and hitting threshold edge case
Browse files Browse the repository at this point in the history
  • Loading branch information
ethanfrey committed Dec 15, 2020
1 parent 6ab0bd7 commit 83406ad
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 7 deletions.
57 changes: 57 additions & 0 deletions contracts/cw3-flex-multisig/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
71 changes: 64 additions & 7 deletions contracts/cw3-flex-multisig/src/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down Expand Up @@ -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)
);
}
}

0 comments on commit 83406ad

Please sign in to comment.