Repository: DA0-DA0/dao-contracts
Pull request #224 (2022-03-25 21:18:25)

Add a majority passing threshold option.
The majority threshold option allows the creator to specify that they
would like a proposal to pass if the majority of votes for that
proposal are yes. Otherwise, a proposal will pass if the yes votes are
greater than or equal to the number of votes required for passing.

Why this is needed:

If the passing threshold is 60% folks likely expect that if 6 out of
10 members vote yes that proposal will pass. This implies that yes >=
threshold.

On the other hand, this makes it difficult to specify that users would
like a majority of votes to be the passing threshold as specifying a
50% passing threshold will allow a vote with 5 yes and 5 no votes to
pass. The majority option addresses this by comparing yes > threshold
for that case.

This PR also adds a special case so if all the votes for a proposal are
abstain the proposal fails.


Patch:
diff --git a/contracts/cw-govmod-single/src/proposal.rs b/contracts/cw-govmod-single/src/proposal.rs
index 5443cdc2..cad126aa 100644
--- a/contracts/cw-govmod-single/src/proposal.rs
+++ b/contracts/cw-govmod-single/src/proposal.rs
@@ -2,12 +2,12 @@ use cosmwasm_std::{Addr, BlockInfo, CosmosMsg, Decimal, Empty, StdResult, Storag
 use cw_utils::Expiration;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
-use voting::{votes_meet_threshold, Votes};
+use voting::{compare_vote_count, VoteCmp, Votes};
 
 use crate::{
     query::ProposalResponse,
     state::{CheckedDepositInfo, PROPOSAL_COUNT},
-    threshold::Threshold,
+    threshold::{PercentageThreshold, Threshold},
 };
 
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug, Copy)]
@@ -55,6 +55,48 @@ pub fn advance_proposal_id(store: &mut dyn Storage) -> StdResult<u64> {
     Ok(id)
 }
 
+pub fn does_vote_count_pass(
+    yes_votes: Uint128,
+    options: Uint128,
+    percent: PercentageThreshold,
+) -> bool {
+    // Don't pass proposals if all the votes are abstain.
+    if options.is_zero() {
+        return false;
+    }
+    match percent {
+        PercentageThreshold::Majority {} => {
+            compare_vote_count(yes_votes, VoteCmp::Greater, options, Decimal::percent(50))
+        }
+        PercentageThreshold::Percent(percent) => {
+            compare_vote_count(yes_votes, VoteCmp::Geq, options, percent)
+        }
+    }
+}
+
+pub fn does_vote_count_fail(
+    no_votes: Uint128,
+    options: Uint128,
+    percent: PercentageThreshold,
+) -> bool {
+    // All abstain votes should result in a rejected proposal.
+    if options.is_zero() {
+        return true;
+    }
+    match percent {
+        PercentageThreshold::Majority {} => {
+            // Fails if no votes have >= half of all votes.
+            compare_vote_count(no_votes, VoteCmp::Geq, options, Decimal::percent(50))
+        }
+        PercentageThreshold::Percent(percent) => compare_vote_count(
+            no_votes,
+            VoteCmp::Greater,
+            options,
+            Decimal::one() - percent,
+        ),
+    }
+}
+
 impl Proposal {
     /// Consumes the proposal and returns a version which may be used
     /// in a query response. The difference being that proposal
@@ -91,15 +133,15 @@ impl Proposal {
     /// it to fail)
     pub fn is_passed(&self, block: &BlockInfo) -> bool {
         match self.threshold {
-            Threshold::AbsolutePercentage { percentage } => votes_meet_threshold(
-                self.votes.yes,
-                self.total_power - self.votes.abstain,
-                percentage,
-            ),
+            Threshold::AbsolutePercentage { percentage } => {
+                let options = self.total_power - self.votes.abstain;
+                does_vote_count_pass(self.votes.yes, options, percentage)
+            }
             Threshold::ThresholdQuorum { threshold, quorum } => {
-                if !votes_meet_threshold(self.votes.total(), self.total_power, quorum) {
+                if !does_vote_count_pass(self.votes.total(), self.total_power, quorum) {
                     return false;
                 }
+
                 if self.expiration.is_expired(block) {
                     // If the quorum is met and the proposal is
                     // expired the number of votes needed to pass a
@@ -114,10 +156,10 @@ impl Proposal {
                     // `token_price * quorum` as opposed to the
                     // 'desired' `token_price * threshold`.
                     let options = self.votes.total() - self.votes.abstain;
-                    votes_meet_threshold(self.votes.yes, options, threshold)
+                    does_vote_count_pass(self.votes.yes, options, threshold)
                 } else {
                     let options = self.total_power - self.votes.abstain;
-                    votes_meet_threshold(self.votes.yes, options, threshold)
+                    does_vote_count_pass(self.votes.yes, options, threshold)
                 }
             }
         }
@@ -133,7 +175,7 @@ impl Proposal {
                 let options = self.total_power - self.votes.abstain;
 
                 // If there is a 100% passing threshold..
-                if percentage_needed == Decimal::percent(100) {
+                if percentage_needed == PercentageThreshold::Percent(Decimal::percent(100)) {
                     if options == Uint128::zero() {
                         // and there are no possible votes (zero
                         // voting power or all abstain), then this
@@ -152,14 +194,11 @@ impl Proposal {
                     }
                 }
 
-                // We need to invert the threshold, when nos exceed
-                // this inverted threshold we know the yes can never
-                // reach the threshold.
-                votes_meet_threshold(self.votes.no, options, Decimal::one() - percentage_needed)
+                does_vote_count_fail(self.votes.no, options, percentage_needed)
             }
             Threshold::ThresholdQuorum { threshold, quorum } => {
                 match (
-                    votes_meet_threshold(self.votes.total(), self.total_power, quorum),
+                    does_vote_count_pass(self.votes.total(), self.total_power, quorum),
                     self.expiration.is_expired(block),
                 ) {
                     // Has met quorum and is expired.
@@ -169,7 +208,7 @@ impl Proposal {
                         let options = self.votes.total() - self.votes.abstain;
 
                         // If there is a 100% passing threshold..
-                        if threshold == Decimal::percent(100) {
+                        if threshold == PercentageThreshold::Percent(Decimal::percent(100)) {
                             if options == Uint128::zero() {
                                 // and there are no possible votes (zero
                                 // voting power or all abstain), then this
@@ -188,8 +227,7 @@ impl Proposal {
                                 return self.votes.no >= Uint128::new(1);
                             }
                         }
-
-                        votes_meet_threshold(self.votes.no, options, Decimal::one() - threshold)
+                        does_vote_count_fail(self.votes.no, options, threshold)
                     }
                     // Has met quorum and is not expired.
                     // | Hasn't met quorum and is not expired.
@@ -199,7 +237,7 @@ impl Proposal {
                         let options = self.total_power - self.votes.abstain;
 
                         // If there is a 100% passing threshold..
-                        if threshold == Decimal::percent(100) {
+                        if threshold == PercentageThreshold::Percent(Decimal::percent(100)) {
                             if options == Uint128::zero() {
                                 // and there are no possible votes (zero
                                 // voting power or all abstain), then this
@@ -219,7 +257,7 @@ impl Proposal {
                             }
                         }
 
-                        votes_meet_threshold(self.votes.no, options, Decimal::one() - threshold)
+                        does_vote_count_fail(self.votes.no, options, threshold)
                     }
                     // Hasn't met quorum requirement and voting has closed => rejected.
                     (false, true) => true,
@@ -296,7 +334,7 @@ mod test {
     #[test]
     fn test_pass_absolute_percentage() {
         let threshold = Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(50),
+            percentage: PercentageThreshold::Majority {},
         };
         let votes = Votes {
             yes: Uint128::new(7),
@@ -304,8 +342,8 @@ mod test {
             abstain: Uint128::new(2),
         };
 
-        // 15 total votes. 7 yes and 2 abstain. 50% threshold. Should
-        // be passed.
+        // 15 total votes. 7 yes and 2 abstain. 50% threshold. This
+        // should pass.
         assert!(check_is_passed(
             threshold.clone(),
             votes.clone(),
@@ -327,7 +365,7 @@ mod test {
     #[test]
     fn test_reject_absolute_percentage() {
         let percent = Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(50),
+            percentage: PercentageThreshold::Majority {},
         };
 
         // 4 YES, 7 NO, 2 ABSTAIN
@@ -381,8 +419,8 @@ mod test {
     #[test]
     fn proposal_passed_quorum() {
         let quorum = Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(40),
+            threshold: PercentageThreshold::Percent(Decimal::percent(50)),
+            quorum: PercentageThreshold::Percent(Decimal::percent(40)),
         };
         // all non-yes votes are counted for quorum
         let passing = Votes {
@@ -470,8 +508,8 @@ mod test {
     #[test]
     fn proposal_rejected_quorum() {
         let quorum = Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(40),
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Percent(Decimal::percent(40)),
         };
         // all non-yes votes are counted for quorum
         let rejecting = Votes {
@@ -580,8 +618,8 @@ mod test {
         // When we pass absolute threshold (everyone else voting no,
         // we pass), but still don't hit quorum.
         let quorum = Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(60),
-            quorum: Decimal::percent(80),
+            threshold: PercentageThreshold::Percent(Decimal::percent(60)),
+            quorum: PercentageThreshold::Percent(Decimal::percent(80)),
         };
 
         // Try 9 yes, 1 no (out of 15) -> 90% voter threshold, 60%
diff --git a/contracts/cw-govmod-single/src/threshold.rs b/contracts/cw-govmod-single/src/threshold.rs
index 3ab017d2..d01c30a0 100644
--- a/contracts/cw-govmod-single/src/threshold.rs
+++ b/contracts/cw-govmod-single/src/threshold.rs
@@ -4,6 +4,39 @@ use serde::{Deserialize, Serialize};
 
 use crate::ContractError;
 
+/// A percentage of voting power that must vote yes for a proposal to
+/// pass. An example of why this is needed:
+///
+/// If a user specifies a 60% passing threshold, and there are 10
+/// voters they likely expect that proposal to pass when there are 6
+/// yes votes. This implies that the condition for passing should be
+/// `yes_votes >= total_votes * threshold`.
+///
+/// With this in mind, how should a user specify that they would like
+/// proposals to pass if the majority of voters choose yes? Selecting
+/// a 50% passing threshold with those rules doesn't properly cover
+/// that case as 5 voters voting yes out of 10 would pass the
+/// proposal. Selecting 50.0001% or or some variation of that also
+/// does not work as a very small yes vote which technically makes the
+/// majority yes may not reach that threshold.
+///
+/// To handle these cases we provide both a majority and percent
+/// option for all percentages. If majority is selected passing will
+/// be determined by `yes > total_votes * 0.5`. If percent is selected
+/// passing is determined by `yes >= total_votes * percent`.
+///
+/// In both of these cases a proposal with only abstain votes must
+/// fail. This requires a special case passing logic.
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, JsonSchema, Debug)]
+#[serde(rename_all = "snake_case")]
+pub enum PercentageThreshold {
+    /// The majority of voters must vote yes for the proposal to pass.
+    Majority {},
+    /// A percentage of voting power >= percent must vote yes for the
+    /// proposal to pass.
+    Percent(Decimal),
+}
+
 /// The ways a proposal may reach its passing / failing threshold.
 #[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
 #[serde(rename_all = "snake_case")]
@@ -12,30 +45,41 @@ pub enum Threshold {
     /// votes in order for a proposal to pass.  See
     /// `ThresholdResponse::AbsolutePercentage` in the cw3 spec for
     /// details.
-    AbsolutePercentage { percentage: Decimal },
+    AbsolutePercentage { percentage: PercentageThreshold },
 
     /// Declares a `quorum` of the total votes that must participate
     /// in the election in order for the vote to be considered at all.
     /// See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for
     /// details.
-    ThresholdQuorum { threshold: Decimal, quorum: Decimal },
+    ThresholdQuorum {
+        threshold: PercentageThreshold,
+        quorum: PercentageThreshold,
+    },
 }
 
 /// Asserts that the 0.0 < percent <= 1.0
-fn validate_percentage(percent: &Decimal) -> Result<(), ContractError> {
-    if percent.is_zero() {
-        Err(ContractError::ZeroThreshold {})
-    } else if *percent > Decimal::one() {
-        Err(ContractError::UnreachableThreshold {})
+fn validate_percentage(percent: &PercentageThreshold) -> Result<(), ContractError> {
+    if let PercentageThreshold::Percent(percent) = percent {
+        if percent.is_zero() {
+            Err(ContractError::ZeroThreshold {})
+        } else if *percent > Decimal::one() {
+            Err(ContractError::UnreachableThreshold {})
+        } else {
+            Ok(())
+        }
     } else {
         Ok(())
     }
 }
 
 /// Asserts that a quorum <= 1. Quorums may be zero.
-fn validate_quorum(quorum: &Decimal) -> Result<(), ContractError> {
-    if *quorum > Decimal::one() {
-        Err(ContractError::UnreachableThreshold {})
+fn validate_quorum(quorum: &PercentageThreshold) -> Result<(), ContractError> {
+    if let PercentageThreshold::Percent(quorum) = quorum {
+        if *quorum > Decimal::one() {
+            Err(ContractError::UnreachableThreshold {})
+        } else {
+            Ok(())
+        }
     } else {
         Ok(())
     }
diff --git a/packages/voting/src/lib.rs b/packages/voting/src/lib.rs
index f22c2557..effcd9cb 100644
--- a/packages/voting/src/lib.rs
+++ b/packages/voting/src/lib.rs
@@ -26,15 +26,32 @@ pub enum Vote {
     Abstain,
 }
 
-/// Information about the number of votes needed to pass a proposal.
-#[derive(PartialEq, Clone, Debug)]
-pub enum VotesNeeded {
-    /// A finite number of votes.
-    Finite(Uint128),
-    /// An unrechable number of votes. Caused by zero total voting
-    /// power.
-    Unreachable,
+pub enum VoteCmp {
+    Greater,
+    Geq,
 }
+
+/// Compares `votes` with `total_power * passing_percentage`. The
+/// comparason function used depends on the `VoteCmp` variation
+/// selected.
+pub fn compare_vote_count(
+    votes: Uint128,
+    cmp: VoteCmp,
+    total_power: Uint128,
+    passing_percentage: Decimal,
+) -> bool {
+    let votes = votes.full_mul(PRECISION_FACTOR);
+    let total_power = total_power.full_mul(PRECISION_FACTOR);
+    let threshold = total_power.multiply_ratio(
+        passing_percentage.atomics(),
+        Uint256::from(10u64).pow(passing_percentage.decimal_places()),
+    );
+    match cmp {
+        VoteCmp::Greater => votes > threshold,
+        VoteCmp::Geq => votes >= threshold,
+    }
+}
+
 /// Computes the number of votes needed for a proposal to pass. This
 /// must round up. For example, with a 50% passing percentage and 15
 /// total votes 8 votes are required, not 7.
@@ -89,16 +106,6 @@ pub fn votes_needed(total_power: Uint128, passing_percentage: Decimal) -> Uint12
     rounded.try_into().unwrap()
 }
 
-/// Determines if a number of votes meets the provided threshold given
-/// the total number of votes outstanding.
-pub fn votes_meet_threshold(
-    votes: Uint128,
-    total_power: Uint128,
-    passing_percentage: Decimal,
-) -> bool {
-    votes >= votes_needed(total_power, passing_percentage)
-}
-
 impl Votes {
     /// Constructs an zero'd out votes struct.
     pub fn zero() -> Self {
@@ -155,6 +162,16 @@ impl std::fmt::Display for Vote {
 mod test {
     use super::*;
 
+    /// Determines if a number of votes meets the provided threshold given
+    /// the total number of votes outstanding.
+    fn votes_meet_threshold(
+        votes: Uint128,
+        total_power: Uint128,
+        passing_percentage: Decimal,
+    ) -> bool {
+        votes >= votes_needed(total_power, passing_percentage)
+    }
+
     #[test]
     fn count_votes() {
         let mut votes = Votes::with_yes(Uint128::new(5));
@@ -231,7 +248,9 @@ mod test {
                 threshold
             ))
         }
-        // Zero votes out of zero total power meet any threshold.
+        /*
+        Zero votes out of zero total power meet any threshold.
+        */
         assert!(votes_meet_threshold(
             Uint128::new(0),
             Uint128::new(0),


Test patch:
diff --git a/contracts/cw-govmod-single/src/tests.rs b/contracts/cw-govmod-single/src/tests.rs
index e22bce37..097bbf2e 100644
--- a/contracts/cw-govmod-single/src/tests.rs
+++ b/contracts/cw-govmod-single/src/tests.rs
@@ -11,7 +11,7 @@ use crate::{
     proposal::{Proposal, Status},
     query::{ProposalListResponse, ProposalResponse, VoteInfo, VoteResponse},
     state::{CheckedDepositInfo, Config},
-    threshold::Threshold,
+    threshold::{PercentageThreshold, Threshold},
 };
 
 const CREATOR_ADDR: &str = "creator";
@@ -125,7 +125,7 @@ fn test_propose() {
     let govmod_id = app.store_code(single_govmod_contract());
 
     let threshold = Threshold::AbsolutePercentage {
-        percentage: Decimal::percent(50),
+        percentage: PercentageThreshold::Majority {},
     };
     let max_voting_period = cw_utils::Duration::Height(6);
     let instantiate = InstantiateMsg {
@@ -399,7 +399,7 @@ fn test_vote_simple() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(100),
+            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
         },
         Status::Passed,
         None,
@@ -414,7 +414,7 @@ fn test_vote_simple() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(100),
+            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
         },
         Status::Rejected,
         None,
@@ -434,7 +434,7 @@ fn test_vote_no_overflow() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(100),
+            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
         },
         Status::Passed,
         None,
@@ -452,9 +452,9 @@ fn test_vote_abstain_only() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(100),
+            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
         },
-        Status::Passed,
+        Status::Rejected,
         None,
         None,
     );
@@ -470,10 +470,10 @@ fn test_vote_abstain_only() {
                 should_execute: ShouldExecute::Yes,
             }],
             Threshold::ThresholdQuorum {
-                threshold: Decimal::percent(100),
-                quorum: Decimal::percent(i),
+                threshold: PercentageThreshold::Percent(Decimal::percent(100)),
+                quorum: PercentageThreshold::Percent(Decimal::percent(i)),
             },
-            Status::Passed,
+            Status::Rejected,
             None,
             None,
         );
@@ -490,7 +490,7 @@ fn test_single_no() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(100),
+            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
         },
         Status::Rejected,
         Some(Uint128::from(u128::max_value())),
@@ -512,7 +512,7 @@ fn test_tricky_rounding() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(1),
+            percentage: PercentageThreshold::Percent(Decimal::percent(1)),
         },
         Status::Passed,
         None,
@@ -527,9 +527,9 @@ fn test_tricky_rounding() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(1),
+            percentage: PercentageThreshold::Percent(Decimal::percent(1)),
         },
-        Status::Passed,
+        Status::Rejected,
         None,
         None,
     );
@@ -553,7 +553,7 @@ fn test_no_double_votes() {
             },
         ],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(100),
+            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
         },
         // NOTE: Updating our cw20-base version will cause this to
         // fail. In versions of cw20-base before Feb 15 2022 (the one
@@ -595,13 +595,13 @@ fn test_close_votes() {
                 voter: "ezek".to_string(),
                 position: Vote::Yes,
                 weight: Uint128::new(5),
-                should_execute: ShouldExecute::No,
+                should_execute: ShouldExecute::Yes,
             },
         ],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(50),
+            percentage: PercentageThreshold::Percent(Decimal::percent(50)),
         },
-        Status::Rejected,
+        Status::Passed,
         None,
         None,
     );
@@ -628,7 +628,7 @@ fn test_close_votes() {
             },
         ],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(50),
+            percentage: PercentageThreshold::Percent(Decimal::percent(50)),
         },
         Status::Passed,
         None,
@@ -668,8 +668,8 @@ fn test_close_votes_quorum() {
             },
         ],
         Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(10),
-            quorum: Decimal::percent(50),
+            threshold: PercentageThreshold::Percent(Decimal::percent(10)),
+            quorum: PercentageThreshold::Majority {},
         },
         Status::Passed,
         None,
@@ -677,6 +677,57 @@ fn test_close_votes_quorum() {
     );
 }
 
+#[test]
+fn test_majority_vs_half() {
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "keze".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+        ],
+        Threshold::ThresholdQuorum {
+            threshold: PercentageThreshold::Percent(Decimal::percent(50)),
+            quorum: PercentageThreshold::Majority {},
+        },
+        Status::Passed,
+        None,
+        None,
+    );
+
+    do_test_votes(
+        vec![
+            TestVote {
+                voter: "ekez".to_string(),
+                position: Vote::No,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::Yes,
+            },
+            TestVote {
+                voter: "keze".to_string(),
+                position: Vote::Yes,
+                weight: Uint128::new(10),
+                should_execute: ShouldExecute::No,
+            },
+        ],
+        Threshold::ThresholdQuorum {
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Majority {},
+        },
+        Status::Rejected,
+        None,
+        None,
+    );
+}
+
 #[test]
 fn test_pass_threshold_not_quorum() {
     do_test_votes(
@@ -687,8 +738,8 @@ fn test_pass_threshold_not_quorum() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(60),
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
         },
         Status::Open,
         Some(Uint128::new(100)),
@@ -702,8 +753,8 @@ fn test_pass_threshold_not_quorum() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(60),
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
         },
         // As the threshold is 50% and 59% of voters have voted no
         // this is unable to pass.
@@ -723,8 +774,8 @@ fn test_pass_threshold_exactly_quorum() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(60),
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
         },
         Status::Passed,
         Some(Uint128::new(100)),
@@ -753,8 +804,8 @@ fn test_pass_threshold_exactly_quorum() {
             },
         ],
         Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(60),
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
         },
         Status::Passed,
         Some(Uint128::new(100)),
@@ -768,8 +819,8 @@ fn test_pass_threshold_exactly_quorum() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::ThresholdQuorum {
-            threshold: Decimal::percent(50),
-            quorum: Decimal::percent(60),
+            threshold: PercentageThreshold::Majority {},
+            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
         },
         Status::Rejected,
         Some(Uint128::new(100)),
@@ -792,7 +843,7 @@ fn fuzz_voting() {
         let expected_status = match yes_sum.cmp(&no_sum) {
             std::cmp::Ordering::Less => Status::Rejected,
             // Depends on which reaches the threshold first. Ignore for now.
-            std::cmp::Ordering::Equal => continue,
+            std::cmp::Ordering::Equal => Status::Rejected,
             std::cmp::Ordering::Greater => Status::Passed,
         };
 
@@ -814,7 +865,7 @@ fn fuzz_voting() {
         do_test_votes(
             votes,
             Threshold::AbsolutePercentage {
-                percentage: Decimal::percent(50),
+                percentage: PercentageThreshold::Majority {},
             },
             expected_status,
             None,
@@ -831,7 +882,7 @@ fn test_voting_module_token_proposal_deposit_instantiate() {
     let govmod_id = app.store_code(single_govmod_contract());
 
     let threshold = Threshold::AbsolutePercentage {
-        percentage: Decimal::percent(50),
+        percentage: PercentageThreshold::Majority {},
     };
     let max_voting_period = cw_utils::Duration::Height(6);
     let instantiate = InstantiateMsg {
@@ -906,7 +957,7 @@ fn test_different_token_proposal_deposit() {
         .unwrap();
 
     let threshold = Threshold::AbsolutePercentage {
-        percentage: Decimal::percent(50),
+        percentage: PercentageThreshold::Majority {},
     };
     let max_voting_period = cw_utils::Duration::Height(6);
     let instantiate = InstantiateMsg {
@@ -961,7 +1012,7 @@ fn test_bad_token_proposal_deposit() {
         .unwrap();
 
     let threshold = Threshold::AbsolutePercentage {
-        percentage: Decimal::percent(50),
+        percentage: PercentageThreshold::Majority {},
     };
     let max_voting_period = cw_utils::Duration::Height(6);
     let instantiate = InstantiateMsg {
@@ -986,7 +1037,7 @@ fn test_take_proposal_deposit() {
     let govmod_id = app.store_code(single_govmod_contract());
 
     let threshold = Threshold::AbsolutePercentage {
-        percentage: Decimal::percent(50),
+        percentage: PercentageThreshold::Majority {},
     };
     let max_voting_period = cw_utils::Duration::Height(6);
     let instantiate = InstantiateMsg {
@@ -1099,7 +1150,7 @@ fn test_deposit_return_on_execute() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(90),
+            percentage: PercentageThreshold::Percent(Decimal::percent(90)),
         },
         Status::Passed,
         None,
@@ -1171,7 +1222,7 @@ fn test_close_open_proposal() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(90),
+            percentage: PercentageThreshold::Percent(Decimal::percent(90)),
         },
         Status::Open,
         Some(Uint128::new(100)),
@@ -1245,7 +1296,7 @@ fn test_deposit_return_on_close() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(90),
+            percentage: PercentageThreshold::Percent(Decimal::percent(90)),
         },
         Status::Rejected,
         None,
@@ -1307,6 +1358,120 @@ fn test_deposit_return_on_close() {
     assert_eq!(balance.balance, Uint128::new(10));
 }
 
+#[test]
+fn test_update_config() {
+    let (mut app, governance_addr) = do_test_votes(
+        vec![TestVote {
+            voter: "ekez".to_string(),
+            position: Vote::No,
+            weight: Uint128::new(10),
+            should_execute: ShouldExecute::Yes,
+        }],
+        Threshold::AbsolutePercentage {
+            percentage: PercentageThreshold::Percent(Decimal::percent(90)),
+        },
+        Status::Rejected,
+        None,
+        Some(DepositInfo {
+            token: DepositToken::VotingModuleToken {},
+            deposit: Uint128::new(1),
+            refund_failed_proposals: false,
+        }),
+    );
+
+    let gov_state: cw_governance::query::DumpStateResponse = app
+        .wrap()
+        .query_wasm_smart(governance_addr, &cw_governance::msg::QueryMsg::DumpState {})
+        .unwrap();
+    let governance_modules = gov_state.governance_modules;
+
+    assert_eq!(governance_modules.len(), 1);
+    let govmod_single = governance_modules.into_iter().next().unwrap();
+
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+
+    assert_eq!(
+        govmod_config.threshold,
+        Threshold::AbsolutePercentage {
+            percentage: PercentageThreshold::Percent(Decimal::percent(90)),
+        }
+    );
+
+    let dao = govmod_config.dao;
+
+    // Attempt to update the config from a non-dao address. This
+    // should fail as it is unauthorized.
+    app.execute_contract(
+        Addr::unchecked("ekez"),
+        govmod_single.clone(),
+        &ExecuteMsg::UpdateConfig {
+            threshold: Threshold::AbsolutePercentage {
+                percentage: PercentageThreshold::Majority {},
+            },
+            max_voting_period: cw_utils::Duration::Height(10),
+            only_members_execute: false,
+            dao: CREATOR_ADDR.to_string(),
+            deposit_info: None,
+        },
+        &[],
+    )
+    .unwrap_err();
+
+    // Update the config from the DAO address. This should succede.
+    app.execute_contract(
+        dao.clone(),
+        govmod_single.clone(),
+        &ExecuteMsg::UpdateConfig {
+            threshold: Threshold::AbsolutePercentage {
+                percentage: PercentageThreshold::Majority {},
+            },
+            max_voting_period: cw_utils::Duration::Height(10),
+            only_members_execute: false,
+            dao: CREATOR_ADDR.to_string(),
+            deposit_info: None,
+        },
+        &[],
+    )
+    .unwrap();
+
+    let govmod_config: Config = app
+        .wrap()
+        .query_wasm_smart(govmod_single.clone(), &QueryMsg::Config {})
+        .unwrap();
+
+    let expected = Config {
+        threshold: Threshold::AbsolutePercentage {
+            percentage: PercentageThreshold::Majority {},
+        },
+        max_voting_period: cw_utils::Duration::Height(10),
+        only_members_execute: false,
+        dao: Addr::unchecked(CREATOR_ADDR),
+        deposit_info: None,
+    };
+    assert_eq!(govmod_config, expected);
+
+    // As we have changed the DAO address updating the config using
+    // the original one should now fail.
+    app.execute_contract(
+        dao,
+        govmod_single,
+        &ExecuteMsg::UpdateConfig {
+            threshold: Threshold::AbsolutePercentage {
+                percentage: PercentageThreshold::Majority {},
+            },
+            max_voting_period: cw_utils::Duration::Height(10),
+            only_members_execute: false,
+            dao: CREATOR_ADDR.to_string(),
+            deposit_info: None,
+        },
+        &[],
+    )
+    .unwrap_err();
+}
+
 #[test]
 fn test_no_return_if_no_refunds() {
     let (mut app, governance_addr) = do_test_votes(
@@ -1317,7 +1482,7 @@ fn test_no_return_if_no_refunds() {
             should_execute: ShouldExecute::Yes,
         }],
         Threshold::AbsolutePercentage {
-            percentage: Decimal::percent(90),
+            percentage: PercentageThreshold::Percent(Decimal::percent(90)),
         },
         Status::Rejected,
         None,
@@ -1375,8 +1540,8 @@ fn test_query_list_proposals() {
         govmod_id,
         InstantiateMsg {
             threshold: Threshold::ThresholdQuorum {
-                threshold: Decimal::percent(50),
-                quorum: Decimal::percent(0),
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(0)),
             },
             max_voting_period: cw_utils::Duration::Height(100),
             only_members_execute: true,
@@ -1451,8 +1616,8 @@ fn test_query_list_proposals() {
             start_height: app.block_info().height,
             expiration: cw_utils::Expiration::AtHeight(app.block_info().height + 100),
             threshold: Threshold::ThresholdQuorum {
-                threshold: Decimal::percent(50),
-                quorum: Decimal::percent(0),
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(0)),
             },
             total_power: Uint128::new(100),
             msgs: vec![],
@@ -1494,8 +1659,8 @@ fn test_query_list_proposals() {
             start_height: app.block_info().height,
             expiration: cw_utils::Expiration::AtHeight(app.block_info().height + 100),
             threshold: Threshold::ThresholdQuorum {
-                threshold: Decimal::percent(50),
-                quorum: Decimal::percent(0),
+                threshold: PercentageThreshold::Majority {},
+                quorum: PercentageThreshold::Percent(Decimal::percent(0)),
             },
             total_power: Uint128::new(100),
             msgs: vec![],