cw_utils/
threshold.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{Decimal, StdError};
3use thiserror::Error;
4
5/// This defines the different ways tallies can happen.
6///
7/// The total_weight used for calculating success as well as the weights of each
8/// individual voter used in tallying should be snapshotted at the beginning of
9/// the block at which the proposal starts (this is likely the responsibility of a
10/// correct cw4 implementation).
11/// See also `ThresholdResponse` in the cw3 spec.
12#[cw_serde]
13pub enum Threshold {
14    /// Declares that a fixed weight of Yes votes is needed to pass.
15    /// See `ThresholdResponse.AbsoluteCount` in the cw3 spec for details.
16    AbsoluteCount { weight: u64 },
17
18    /// Declares a percentage of the total weight that must cast Yes votes in order for
19    /// a proposal to pass.
20    /// See `ThresholdResponse.AbsolutePercentage` in the cw3 spec for details.
21    AbsolutePercentage { percentage: Decimal },
22
23    /// Declares a `quorum` of the total votes that must participate in the election in order
24    /// for the vote to be considered at all.
25    /// See `ThresholdResponse.ThresholdQuorum` in the cw3 spec for details.
26    ThresholdQuorum { threshold: Decimal, quorum: Decimal },
27}
28
29impl Threshold {
30    /// returns error if this is an unreachable value,
31    /// given a total weight of all members in the group
32    pub fn validate(&self, total_weight: u64) -> Result<(), ThresholdError> {
33        match self {
34            Threshold::AbsoluteCount {
35                weight: weight_needed,
36            } => {
37                if *weight_needed == 0 {
38                    Err(ThresholdError::ZeroWeight {})
39                } else if *weight_needed > total_weight {
40                    Err(ThresholdError::UnreachableWeight {})
41                } else {
42                    Ok(())
43                }
44            }
45            Threshold::AbsolutePercentage {
46                percentage: percentage_needed,
47            } => valid_threshold(percentage_needed),
48            Threshold::ThresholdQuorum {
49                threshold,
50                quorum: quroum,
51            } => {
52                valid_threshold(threshold)?;
53                valid_quorum(quroum)
54            }
55        }
56    }
57
58    /// Creates a response from the saved data, just missing the total_weight info
59    pub fn to_response(&self, total_weight: u64) -> ThresholdResponse {
60        match self.clone() {
61            Threshold::AbsoluteCount { weight } => ThresholdResponse::AbsoluteCount {
62                weight,
63                total_weight,
64            },
65            Threshold::AbsolutePercentage { percentage } => ThresholdResponse::AbsolutePercentage {
66                percentage,
67                total_weight,
68            },
69            Threshold::ThresholdQuorum { threshold, quorum } => {
70                ThresholdResponse::ThresholdQuorum {
71                    threshold,
72                    quorum,
73                    total_weight,
74                }
75            }
76        }
77    }
78}
79
80/// Asserts that the 0.5 < percent <= 1.0
81fn valid_threshold(percent: &Decimal) -> Result<(), ThresholdError> {
82    if *percent > Decimal::percent(100) || *percent < Decimal::percent(50) {
83        Err(ThresholdError::InvalidThreshold {})
84    } else {
85        Ok(())
86    }
87}
88
89/// Asserts that the 0.5 < percent <= 1.0
90fn valid_quorum(percent: &Decimal) -> Result<(), ThresholdError> {
91    if percent.is_zero() {
92        Err(ThresholdError::ZeroQuorumThreshold {})
93    } else if *percent > Decimal::one() {
94        Err(ThresholdError::UnreachableQuorumThreshold {})
95    } else {
96        Ok(())
97    }
98}
99
100/// This defines the different ways tallies can happen.
101/// Every contract should support a subset of these, ideally all.
102///
103/// The total_weight used for calculating success as well as the weights of each
104/// individual voter used in tallying should be snapshotted at the beginning of
105/// the block at which the proposal starts (this is likely the responsibility of a
106/// correct cw4 implementation).
107#[cw_serde]
108pub enum ThresholdResponse {
109    /// Declares that a fixed weight of yes votes is needed to pass.
110    /// It does not matter how many no votes are cast, or how many do not vote,
111    /// as long as `weight` yes votes are cast.
112    ///
113    /// This is the simplest format and usually suitable for small multisigs of trusted parties,
114    /// like 3 of 5. (weight: 3, total_weight: 5)
115    ///
116    /// A proposal of this type can pass early as soon as the needed weight of yes votes has been cast.
117    AbsoluteCount { weight: u64, total_weight: u64 },
118
119    /// Declares a percentage of the total weight that must cast Yes votes, in order for
120    /// a proposal to pass. The passing weight is computed over the total weight minus the weight of the
121    /// abstained votes.
122    ///
123    /// This is useful for similar circumstances as `AbsoluteCount`, where we have a relatively
124    /// small set of voters, and participation is required.
125    /// It is understood that if the voting set (group) changes between different proposals that
126    /// refer to the same group, each proposal will work with a different set of voter weights
127    /// (the ones snapshotted at proposal creation), and the passing weight for each proposal
128    /// will be computed based on the absolute percentage, times the total weights of the members
129    /// at the time of each proposal creation.
130    ///
131    /// Example: we set `percentage` to 51%. Proposal 1 starts when there is a `total_weight` of 5.
132    /// This will require 3 weight of Yes votes in order to pass. Later, the Proposal 2 starts but the
133    /// `total_weight` of the group has increased to 9. That proposal will then automatically
134    /// require 5 Yes of 9 to pass, rather than 3 yes of 9 as would be the case with `AbsoluteCount`.
135    AbsolutePercentage {
136        percentage: Decimal,
137        total_weight: u64,
138    },
139
140    /// In addition to a `threshold`, declares a `quorum` of the total votes that must participate
141    /// in the election in order for the vote to be considered at all. Within the votes that
142    /// were cast, it requires `threshold` votes in favor. That is calculated by ignoring
143    /// the Abstain votes (they count towards `quorum`, but do not influence `threshold`).
144    /// That is, we calculate `Yes / (Yes + No + Veto)` and compare it with `threshold` to consider
145    /// if the proposal was passed.
146    ///
147    /// It is rather difficult for a proposal of this type to pass early. That can only happen if
148    /// the required quorum has been already met, and there are already enough Yes votes for the
149    /// proposal to pass.
150    ///
151    /// 30% Yes votes, 10% No votes, and 20% Abstain would pass early if quorum <= 60%
152    /// (who has cast votes) and if the threshold is <= 37.5% (the remaining 40% voting
153    /// no => 30% yes + 50% no). Once the voting period has passed with no additional votes,
154    /// that same proposal would be considered successful if quorum <= 60% and threshold <= 75%
155    /// (percent in favor if we ignore abstain votes).
156    ///
157    /// This type is more common in general elections, where participation is often expected to
158    /// be low, and `AbsolutePercentage` would either be too high to pass anything,
159    /// or allow low percentages to pass, independently of if there was high participation in the
160    /// election or not.
161    ThresholdQuorum {
162        threshold: Decimal,
163        quorum: Decimal,
164        total_weight: u64,
165    },
166}
167
168#[derive(Error, Debug)]
169pub enum ThresholdError {
170    #[error("{0}")]
171    Std(#[from] StdError),
172
173    #[error("Invalid voting threshold percentage, must be in the 0.5-1.0 range")]
174    InvalidThreshold {},
175
176    #[error("Required quorum threshold cannot be zero")]
177    ZeroQuorumThreshold {},
178
179    #[error("Not possible to reach required quorum threshold")]
180    UnreachableQuorumThreshold {},
181
182    #[error("Required weight cannot be zero")]
183    ZeroWeight {},
184
185    #[error("Not possible to reach required (passing) weight")]
186    UnreachableWeight {},
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn validate_quorum_percentage() {
195        // TODO: test the error messages
196
197        // 0 is never a valid percentage
198        let err = valid_quorum(&Decimal::zero()).unwrap_err();
199        assert_eq!(
200            err.to_string(),
201            ThresholdError::ZeroQuorumThreshold {}.to_string()
202        );
203
204        // 100% is
205        valid_quorum(&Decimal::one()).unwrap();
206
207        // 101% is not
208        let err = valid_quorum(&Decimal::percent(101)).unwrap_err();
209        assert_eq!(
210            err.to_string(),
211            ThresholdError::UnreachableQuorumThreshold {}.to_string()
212        );
213        // not 100.1%
214        let err = valid_quorum(&Decimal::permille(1001)).unwrap_err();
215        assert_eq!(
216            err.to_string(),
217            ThresholdError::UnreachableQuorumThreshold {}.to_string()
218        );
219    }
220
221    #[test]
222    fn validate_threshold_percentage() {
223        // other values in between 0.5 and 1 are valid
224        valid_threshold(&Decimal::percent(51)).unwrap();
225        valid_threshold(&Decimal::percent(67)).unwrap();
226        valid_threshold(&Decimal::percent(99)).unwrap();
227        let err = valid_threshold(&Decimal::percent(101)).unwrap_err();
228        assert_eq!(
229            err.to_string(),
230            ThresholdError::InvalidThreshold {}.to_string()
231        );
232    }
233
234    #[test]
235    fn validate_threshold() {
236        // absolute count ensures 0 < required <= total_weight
237        let err = Threshold::AbsoluteCount { weight: 0 }
238            .validate(5)
239            .unwrap_err();
240        // TODO: remove to_string() when PartialEq implemented
241        assert_eq!(err.to_string(), ThresholdError::ZeroWeight {}.to_string());
242        let err = Threshold::AbsoluteCount { weight: 6 }
243            .validate(5)
244            .unwrap_err();
245        assert_eq!(
246            err.to_string(),
247            ThresholdError::UnreachableWeight {}.to_string()
248        );
249
250        Threshold::AbsoluteCount { weight: 1 }.validate(5).unwrap();
251        Threshold::AbsoluteCount { weight: 5 }.validate(5).unwrap();
252
253        // AbsolutePercentage just enforces valid_percentage (tested above)
254        let err = Threshold::AbsolutePercentage {
255            percentage: Decimal::zero(),
256        }
257        .validate(5)
258        .unwrap_err();
259        assert_eq!(
260            err.to_string(),
261            ThresholdError::InvalidThreshold {}.to_string()
262        );
263        Threshold::AbsolutePercentage {
264            percentage: Decimal::percent(51),
265        }
266        .validate(5)
267        .unwrap();
268
269        // Quorum enforces both valid just enforces valid_percentage (tested above)
270        Threshold::ThresholdQuorum {
271            threshold: Decimal::percent(51),
272            quorum: Decimal::percent(40),
273        }
274        .validate(5)
275        .unwrap();
276        let err = Threshold::ThresholdQuorum {
277            threshold: Decimal::percent(101),
278            quorum: Decimal::percent(40),
279        }
280        .validate(5)
281        .unwrap_err();
282        assert_eq!(
283            err.to_string(),
284            ThresholdError::InvalidThreshold {}.to_string()
285        );
286        let err = Threshold::ThresholdQuorum {
287            threshold: Decimal::percent(51),
288            quorum: Decimal::percent(0),
289        }
290        .validate(5)
291        .unwrap_err();
292        assert_eq!(
293            err.to_string(),
294            ThresholdError::ZeroQuorumThreshold {}.to_string()
295        );
296    }
297
298    #[test]
299    fn threshold_response() {
300        let total_weight: u64 = 100;
301
302        let res = Threshold::AbsoluteCount { weight: 42 }.to_response(total_weight);
303        assert_eq!(
304            res,
305            ThresholdResponse::AbsoluteCount {
306                weight: 42,
307                total_weight
308            }
309        );
310
311        let res = Threshold::AbsolutePercentage {
312            percentage: Decimal::percent(51),
313        }
314        .to_response(total_weight);
315        assert_eq!(
316            res,
317            ThresholdResponse::AbsolutePercentage {
318                percentage: Decimal::percent(51),
319                total_weight
320            }
321        );
322
323        let res = Threshold::ThresholdQuorum {
324            threshold: Decimal::percent(66),
325            quorum: Decimal::percent(50),
326        }
327        .to_response(total_weight);
328        assert_eq!(
329            res,
330            ThresholdResponse::ThresholdQuorum {
331                threshold: Decimal::percent(66),
332                quorum: Decimal::percent(50),
333                total_weight
334            }
335        );
336    }
337}