dao_voting/
threshold.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{Decimal, Uint128};
3
4use thiserror::Error;
5
6/// The threshold of tokens that must be staked in order for this
7/// voting module to be active. If this is not reached, this module
8/// will response to `is_active` queries with false and proposal
9/// modules which respect active thresholds will not allow the
10/// creation of proposals.
11#[cw_serde]
12pub enum ActiveThreshold {
13    /// The absolute number of tokens that must be staked for the
14    /// module to be active.
15    AbsoluteCount { count: Uint128 },
16    /// The percentage of tokens that must be staked for the module to
17    /// be active. Computed as `staked / total_supply`.
18    Percentage { percent: Decimal },
19}
20
21#[cw_serde]
22pub struct ActiveThresholdResponse {
23    pub active_threshold: Option<ActiveThreshold>,
24}
25
26#[derive(Error, Debug, PartialEq, Eq)]
27pub enum ActiveThresholdError {
28    #[error("Absolute count threshold cannot be greater than the total token supply")]
29    InvalidAbsoluteCount {},
30
31    #[error("Active threshold percentage must be greater than 0 and not greater than 1")]
32    InvalidActivePercentage {},
33
34    #[error("Active threshold count must be greater than zero")]
35    ZeroActiveCount {},
36}
37
38pub fn assert_valid_absolute_count_threshold(
39    count: Uint128,
40    supply: Uint128,
41) -> Result<(), ActiveThresholdError> {
42    if count.is_zero() {
43        return Err(ActiveThresholdError::ZeroActiveCount {});
44    }
45    if count > supply {
46        return Err(ActiveThresholdError::InvalidAbsoluteCount {});
47    }
48    Ok(())
49}
50
51pub fn assert_valid_percentage_threshold(percent: Decimal) -> Result<(), ActiveThresholdError> {
52    if percent.is_zero() || percent > Decimal::one() {
53        return Err(ActiveThresholdError::InvalidActivePercentage {});
54    }
55    Ok(())
56}
57
58#[derive(Error, Debug, PartialEq, Eq)]
59pub enum ThresholdError {
60    #[error("Required threshold cannot be zero")]
61    ZeroThreshold {},
62
63    #[error("Not possible to reach required (passing) threshold")]
64    UnreachableThreshold {},
65}
66
67/// A percentage of voting power that must vote yes for a proposal to
68/// pass. An example of why this is needed:
69///
70/// If a user specifies a 60% passing threshold, and there are 10
71/// voters they likely expect that proposal to pass when there are 6
72/// yes votes. This implies that the condition for passing should be
73/// `vote_weights >= total_votes * threshold`.
74///
75/// With this in mind, how should a user specify that they would like
76/// proposals to pass if the majority of voters choose yes? Selecting
77/// a 50% passing threshold with those rules doesn't properly cover
78/// that case as 5 voters voting yes out of 10 would pass the
79/// proposal. Selecting 50.0001% or or some variation of that also
80/// does not work as a very small yes vote which technically makes the
81/// majority yes may not reach that threshold.
82///
83/// To handle these cases we provide both a majority and percent
84/// option for all percentages. If majority is selected passing will
85/// be determined by `yes > total_votes * 0.5`. If percent is selected
86/// passing is determined by `yes >= total_votes * percent`.
87///
88/// In both of these cases a proposal with only abstain votes must
89/// fail. This requires a special case passing logic.
90#[cw_serde]
91#[derive(Copy)]
92pub enum PercentageThreshold {
93    /// The majority of voters must vote yes for the proposal to pass.
94    Majority {},
95    /// A percentage of voting power >= percent must vote yes for the
96    /// proposal to pass.
97    Percent(Decimal),
98}
99
100/// The ways a proposal may reach its passing / failing threshold.
101#[cw_serde]
102pub enum Threshold {
103    /// Declares a percentage of the total weight that must cast Yes
104    /// votes in order for a proposal to pass.  See
105    /// `ThresholdResponse::AbsolutePercentage` in the cw3 spec for
106    /// details.
107    AbsolutePercentage { percentage: PercentageThreshold },
108
109    /// Declares a `quorum` of the total votes that must participate
110    /// in the election in order for the vote to be considered at all.
111    /// See `ThresholdResponse::ThresholdQuorum` in the cw3 spec for
112    /// details.
113    ThresholdQuorum {
114        threshold: PercentageThreshold,
115        quorum: PercentageThreshold,
116    },
117
118    /// An absolute number of votes needed for something to cross the
119    /// threshold. Useful for multisig style voting.
120    AbsoluteCount { threshold: Uint128 },
121}
122
123/// Asserts that the 0.0 < percent <= 1.0
124fn validate_percentage(percent: &PercentageThreshold) -> Result<(), ThresholdError> {
125    if let PercentageThreshold::Percent(percent) = percent {
126        if percent.is_zero() {
127            Err(ThresholdError::ZeroThreshold {})
128        } else if *percent > Decimal::one() {
129            Err(ThresholdError::UnreachableThreshold {})
130        } else {
131            Ok(())
132        }
133    } else {
134        Ok(())
135    }
136}
137
138/// Asserts that a quorum <= 1. Quorums may be zero, to enable plurality-style voting.
139pub fn validate_quorum(quorum: &PercentageThreshold) -> Result<(), ThresholdError> {
140    match quorum {
141        PercentageThreshold::Majority {} => Ok(()),
142        PercentageThreshold::Percent(quorum) => {
143            if *quorum > Decimal::one() {
144                Err(ThresholdError::UnreachableThreshold {})
145            } else {
146                Ok(())
147            }
148        }
149    }
150}
151
152impl Threshold {
153    /// Validates the threshold.
154    ///
155    /// - Quorums must never be over 100%.
156    /// - Passing thresholds must never be over 100%, nor be 0%.
157    /// - Absolute count thresholds must be non-zero.
158    pub fn validate(&self) -> Result<(), ThresholdError> {
159        match self {
160            Threshold::AbsolutePercentage {
161                percentage: percentage_needed,
162            } => validate_percentage(percentage_needed),
163            Threshold::ThresholdQuorum { threshold, quorum } => {
164                validate_percentage(threshold)?;
165                validate_quorum(quorum)
166            }
167            Threshold::AbsoluteCount { threshold } => {
168                if threshold.is_zero() {
169                    Err(ThresholdError::ZeroThreshold {})
170                } else {
171                    Ok(())
172                }
173            }
174        }
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    macro_rules! p {
183        ($x:expr ) => {
184            PercentageThreshold::Percent(Decimal::percent($x))
185        };
186    }
187
188    #[test]
189    fn test_threshold_validation() {
190        let t = Threshold::AbsoluteCount {
191            threshold: Uint128::zero(),
192        };
193        assert_eq!(t.validate().unwrap_err(), ThresholdError::ZeroThreshold {});
194
195        let t = Threshold::AbsolutePercentage { percentage: p!(0) };
196        assert_eq!(t.validate().unwrap_err(), ThresholdError::ZeroThreshold {});
197
198        let t = Threshold::AbsolutePercentage {
199            percentage: p!(101),
200        };
201        assert_eq!(
202            t.validate().unwrap_err(),
203            ThresholdError::UnreachableThreshold {}
204        );
205
206        let t = Threshold::AbsolutePercentage {
207            percentage: p!(100),
208        };
209        t.validate().unwrap();
210
211        let t = Threshold::ThresholdQuorum {
212            threshold: p!(101),
213            quorum: p!(0),
214        };
215        assert_eq!(
216            t.validate().unwrap_err(),
217            ThresholdError::UnreachableThreshold {}
218        );
219
220        let t = Threshold::ThresholdQuorum {
221            threshold: p!(100),
222            quorum: p!(0),
223        };
224        t.validate().unwrap();
225
226        let t = Threshold::ThresholdQuorum {
227            threshold: p!(100),
228            quorum: p!(101),
229        };
230        assert_eq!(
231            t.validate().unwrap_err(),
232            ThresholdError::UnreachableThreshold {}
233        );
234    }
235}