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}