diff --git a/smartcontract/contracts/governance/src/lib.rs b/smartcontract/contracts/governance/src/lib.rs index 00f8cb7..bc865fa 100644 --- a/smartcontract/contracts/governance/src/lib.rs +++ b/smartcontract/contracts/governance/src/lib.rs @@ -498,7 +498,12 @@ impl GovernanceContract { .get(&DataKey::QuorumPercent) .unwrap_or(50); - let quorum_threshold = (members.len() * quorum_percent) / 100; + let quorum_threshold = members + .len() + .checked_mul(quorum_percent) + .and_then(|votes| votes.checked_add(99)) + .ok_or(Error::InvalidProposal)? + / 100; if proposal.total_votes < quorum_threshold { proposal.status = ProposalStatus::Expired; } else if proposal.votes_for > proposal.votes_against { diff --git a/smartcontract/contracts/integration-tests/tests/cross_contract_invariants.rs b/smartcontract/contracts/integration-tests/tests/cross_contract_invariants.rs index 2315f3f..911990f 100644 --- a/smartcontract/contracts/integration-tests/tests/cross_contract_invariants.rs +++ b/smartcontract/contracts/integration-tests/tests/cross_contract_invariants.rs @@ -91,6 +91,7 @@ fn governance_membership_lifecycle_updates_authorization() { &newbie, ); gov.vote(&members[0], &add, &true); + gov.vote(&members[1], &add, &true); advance_seq(&env, 100); assert_eq!(gov.finalize(&members[0], &add), ProposalStatus::Passed); gov.execute_proposal(&admin, &add); @@ -116,6 +117,7 @@ fn governance_membership_lifecycle_updates_authorization() { &members[1], ); gov.vote(&members[0], &rem, &true); + gov.vote(&members[2], &rem, &true); advance_seq(&env, 100); assert_eq!(gov.finalize(&members[0], &rem), ProposalStatus::Passed); gov.execute_proposal(&admin, &rem); @@ -175,6 +177,7 @@ fn terminal_operations_cannot_replay_across_contracts() { &gmembers[0], ); gov.vote(&gmembers[0], &pid, &true); + gov.vote(&gmembers[1], &pid, &true); advance_seq(&env, 100); gov.finalize(&gmembers[0], &pid); gov.execute_proposal(&admin, &pid); diff --git a/smartcontract/contracts/integration-tests/tests/governance_invariants.rs b/smartcontract/contracts/integration-tests/tests/governance_invariants.rs index be4aff5..f45904a 100644 --- a/smartcontract/contracts/integration-tests/tests/governance_invariants.rs +++ b/smartcontract/contracts/integration-tests/tests/governance_invariants.rs @@ -96,6 +96,17 @@ fn quorum_not_met_expires() { ); } +/// INV-G4: quorum rounds up so a 50% quorum in a three-member DAO needs two votes. +#[test] +fn quorum_rounds_up_for_non_divisible_member_counts() { + let (env, _admin, members, c) = setup(3, 50, 100); + let id = mk_proposal(&c, &members[0]); + c.vote(&members[0], &id, &true); + + advance_seq(&env, 200); + assert_eq!(c.finalize(&members[0], &id), ProposalStatus::Expired); +} + /// INV-G4: voting is closed once the proposal is finalized. #[test] fn cannot_vote_after_finalize() {