dao_testing/
tests.rs

1use cosmwasm_std::{Decimal, Uint128};
2use dao_voting::status::Status;
3use dao_voting::threshold::{PercentageThreshold, Threshold};
4use dao_voting::voting::Vote;
5use rand::{prelude::SliceRandom, Rng};
6
7/// If a test vote should execute. Used for fuzzing and checking that
8/// votes after a proposal has completed aren't allowed.
9pub enum ShouldExecute {
10    /// This should execute.
11    Yes,
12    /// This should not execute.
13    No,
14    /// Doesn't matter.
15    Meh,
16}
17
18pub struct TestSingleChoiceVote {
19    /// The address casting the vote.
20    pub voter: String,
21    /// Position on the vote.
22    pub position: Vote,
23    /// Voting power of the address.
24    pub weight: Uint128,
25    /// If this vote is expected to execute.
26    pub should_execute: ShouldExecute,
27}
28
29pub fn test_simple_votes<F>(do_votes: F)
30where
31    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
32{
33    do_votes(
34        vec![TestSingleChoiceVote {
35            voter: "ekez".to_string(),
36            position: Vote::Yes,
37            weight: Uint128::new(10),
38            should_execute: ShouldExecute::Yes,
39        }],
40        Threshold::AbsolutePercentage {
41            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
42        },
43        Status::Passed,
44        None,
45    );
46
47    do_votes(
48        vec![TestSingleChoiceVote {
49            voter: "ekez".to_string(),
50            position: Vote::No,
51            weight: Uint128::new(10),
52            should_execute: ShouldExecute::Yes,
53        }],
54        Threshold::AbsolutePercentage {
55            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
56        },
57        Status::Rejected,
58        None,
59    )
60}
61
62pub fn test_simple_vote_no_overflow<F>(do_votes: F)
63where
64    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
65{
66    do_votes(
67        vec![TestSingleChoiceVote {
68            voter: "ekez".to_string(),
69            position: Vote::Yes,
70            weight: Uint128::new(u128::MAX),
71            should_execute: ShouldExecute::Yes,
72        }],
73        Threshold::AbsolutePercentage {
74            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
75        },
76        Status::Passed,
77        None,
78    );
79}
80
81pub fn test_vote_no_overflow<F>(do_votes: F)
82where
83    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
84{
85    do_votes(
86        vec![TestSingleChoiceVote {
87            voter: "ekez".to_string(),
88            position: Vote::Yes,
89            weight: Uint128::new(u128::MAX),
90            should_execute: ShouldExecute::Yes,
91        }],
92        Threshold::AbsolutePercentage {
93            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
94        },
95        Status::Passed,
96        None,
97    );
98
99    do_votes(
100        vec![
101            TestSingleChoiceVote {
102                voter: "zeke".to_string(),
103                position: Vote::No,
104                weight: Uint128::new(1),
105                should_execute: ShouldExecute::Yes,
106            },
107            TestSingleChoiceVote {
108                voter: "ekez".to_string(),
109                position: Vote::Yes,
110                weight: Uint128::new(u128::MAX - 1),
111                should_execute: ShouldExecute::Yes,
112            },
113        ],
114        Threshold::AbsolutePercentage {
115            percentage: PercentageThreshold::Percent(Decimal::percent(99)),
116        },
117        Status::Passed,
118        None,
119    )
120}
121
122pub fn test_simple_early_rejection<F>(do_votes: F)
123where
124    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
125{
126    do_votes(
127        vec![TestSingleChoiceVote {
128            voter: "zeke".to_string(),
129            position: Vote::No,
130            weight: Uint128::new(1),
131            should_execute: ShouldExecute::Yes,
132        }],
133        Threshold::AbsolutePercentage {
134            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
135        },
136        Status::Rejected,
137        None,
138    );
139
140    do_votes(
141        vec![TestSingleChoiceVote {
142            voter: "ekez".to_string(),
143            position: Vote::No,
144            weight: Uint128::new(1),
145            should_execute: ShouldExecute::Yes,
146        }],
147        Threshold::AbsolutePercentage {
148            percentage: PercentageThreshold::Percent(Decimal::percent(99)),
149        },
150        Status::Open,
151        Some(Uint128::from(u128::MAX)),
152    );
153}
154
155pub fn test_vote_abstain_only<F>(do_votes: F)
156where
157    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
158{
159    do_votes(
160        vec![TestSingleChoiceVote {
161            voter: "ekez".to_string(),
162            position: Vote::Abstain,
163            weight: Uint128::new(u64::MAX.into()),
164            should_execute: ShouldExecute::Yes,
165        }],
166        Threshold::AbsolutePercentage {
167            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
168        },
169        Status::Rejected,
170        None,
171    );
172
173    // The quorum shouldn't matter here in determining if the vote is
174    // rejected.
175    for i in 0..101 {
176        do_votes(
177            vec![TestSingleChoiceVote {
178                voter: "ekez".to_string(),
179                position: Vote::Abstain,
180                weight: Uint128::new(u64::MAX.into()),
181                should_execute: ShouldExecute::Yes,
182            }],
183            Threshold::ThresholdQuorum {
184                threshold: PercentageThreshold::Percent(Decimal::percent(100)),
185                quorum: PercentageThreshold::Percent(Decimal::percent(i)),
186            },
187            Status::Rejected,
188            None,
189        );
190    }
191}
192
193pub fn test_tricky_rounding<F>(do_votes: F)
194where
195    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
196{
197    // This tests the smallest possible round up for passing
198    // thresholds we can have. Specifically, a 1% passing threshold
199    // and 1 total vote. This should round up and only pass if there
200    // are more than 1 yes votes.
201    do_votes(
202        vec![TestSingleChoiceVote {
203            voter: "ekez".to_string(),
204            position: Vote::Yes,
205            weight: Uint128::new(1),
206            should_execute: ShouldExecute::Yes,
207        }],
208        Threshold::AbsolutePercentage {
209            percentage: PercentageThreshold::Percent(Decimal::percent(1)),
210        },
211        Status::Passed,
212        Some(Uint128::new(100)),
213    );
214
215    do_votes(
216        vec![TestSingleChoiceVote {
217            voter: "ekez".to_string(),
218            position: Vote::Yes,
219            weight: Uint128::new(10),
220            should_execute: ShouldExecute::Yes,
221        }],
222        Threshold::AbsolutePercentage {
223            percentage: PercentageThreshold::Percent(Decimal::percent(1)),
224        },
225        Status::Passed,
226        Some(Uint128::new(1000)),
227    );
228
229    // HIGH PERCISION
230    do_votes(
231        vec![TestSingleChoiceVote {
232            voter: "ekez".to_string(),
233            position: Vote::Yes,
234            weight: Uint128::new(9999999),
235            should_execute: ShouldExecute::Yes,
236        }],
237        Threshold::AbsolutePercentage {
238            percentage: PercentageThreshold::Percent(Decimal::percent(1)),
239        },
240        Status::Open,
241        Some(Uint128::new(1000000000)),
242    );
243
244    do_votes(
245        vec![TestSingleChoiceVote {
246            voter: "ekez".to_string(),
247            position: Vote::Abstain,
248            weight: Uint128::new(1),
249            should_execute: ShouldExecute::Yes,
250        }],
251        Threshold::AbsolutePercentage {
252            percentage: PercentageThreshold::Percent(Decimal::percent(1)),
253        },
254        Status::Rejected,
255        None,
256    );
257}
258
259pub fn test_no_double_votes<F>(do_votes: F)
260where
261    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
262{
263    do_votes(
264        vec![
265            TestSingleChoiceVote {
266                voter: "ekez".to_string(),
267                position: Vote::Abstain,
268                weight: Uint128::new(2),
269                should_execute: ShouldExecute::Yes,
270            },
271            TestSingleChoiceVote {
272                voter: "ekez".to_string(),
273                position: Vote::Yes,
274                weight: Uint128::new(2),
275                should_execute: ShouldExecute::No,
276            },
277        ],
278        Threshold::AbsolutePercentage {
279            percentage: PercentageThreshold::Percent(Decimal::percent(100)),
280        },
281        // NOTE: Updating our cw20-base version will cause this to
282        // fail. In versions of cw20-base before Feb 15 2022 (the one
283        // we use at the time of writing) it was allowed to have an
284        // initial balance that repeats for a given address but it
285        // would cause miscalculation of the total supply. In this
286        // case the total supply is miscumputed to be 4 so this is
287        // assumed to have 2 abstain votes out of 4 possible votes.
288        Status::Open,
289        Some(Uint128::new(10)),
290    )
291}
292
293pub fn test_votes_favor_yes<F>(do_votes: F)
294where
295    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
296{
297    do_votes(
298        vec![
299            TestSingleChoiceVote {
300                voter: "ekez".to_string(),
301                position: Vote::Abstain,
302                weight: Uint128::new(10),
303                should_execute: ShouldExecute::Yes,
304            },
305            TestSingleChoiceVote {
306                voter: "keze".to_string(),
307                position: Vote::No,
308                weight: Uint128::new(5),
309                should_execute: ShouldExecute::Yes,
310            },
311            TestSingleChoiceVote {
312                voter: "ezek".to_string(),
313                position: Vote::Yes,
314                weight: Uint128::new(5),
315                should_execute: ShouldExecute::Yes,
316            },
317        ],
318        Threshold::AbsolutePercentage {
319            percentage: PercentageThreshold::Percent(Decimal::percent(50)),
320        },
321        Status::Passed,
322        None,
323    );
324
325    do_votes(
326        vec![
327            TestSingleChoiceVote {
328                voter: "ekez".to_string(),
329                position: Vote::Abstain,
330                weight: Uint128::new(10),
331                should_execute: ShouldExecute::Yes,
332            },
333            TestSingleChoiceVote {
334                voter: "keze".to_string(),
335                position: Vote::Yes,
336                weight: Uint128::new(5),
337                should_execute: ShouldExecute::Yes,
338            },
339        ],
340        Threshold::AbsolutePercentage {
341            percentage: PercentageThreshold::Percent(Decimal::percent(50)),
342        },
343        Status::Passed,
344        None,
345    );
346
347    do_votes(
348        vec![
349            TestSingleChoiceVote {
350                voter: "ekez".to_string(),
351                position: Vote::Abstain,
352                weight: Uint128::new(10),
353                should_execute: ShouldExecute::Yes,
354            },
355            TestSingleChoiceVote {
356                voter: "keze".to_string(),
357                position: Vote::Yes,
358                weight: Uint128::new(5),
359                should_execute: ShouldExecute::Yes,
360            },
361            // Can vote up to expiration time.
362            TestSingleChoiceVote {
363                voter: "ezek".to_string(),
364                position: Vote::No,
365                weight: Uint128::new(5),
366                should_execute: ShouldExecute::Yes,
367            },
368        ],
369        Threshold::AbsolutePercentage {
370            percentage: PercentageThreshold::Percent(Decimal::percent(50)),
371        },
372        Status::Passed,
373        None,
374    );
375}
376
377pub fn test_votes_low_threshold<F>(do_votes: F)
378where
379    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
380{
381    do_votes(
382        vec![
383            TestSingleChoiceVote {
384                voter: "ekez".to_string(),
385                position: Vote::No,
386                weight: Uint128::new(10),
387                should_execute: ShouldExecute::Yes,
388            },
389            TestSingleChoiceVote {
390                voter: "keze".to_string(),
391                position: Vote::Yes,
392                weight: Uint128::new(5),
393                should_execute: ShouldExecute::Yes,
394            },
395        ],
396        Threshold::ThresholdQuorum {
397            threshold: PercentageThreshold::Percent(Decimal::percent(10)),
398            quorum: PercentageThreshold::Majority {},
399        },
400        Status::Passed,
401        None,
402    );
403
404    do_votes(
405        vec![
406            TestSingleChoiceVote {
407                voter: "ekez".to_string(),
408                position: Vote::No,
409                weight: Uint128::new(10),
410                should_execute: ShouldExecute::Yes,
411            },
412            TestSingleChoiceVote {
413                voter: "keze".to_string(),
414                position: Vote::Yes,
415                weight: Uint128::new(5),
416                should_execute: ShouldExecute::Yes,
417            },
418            // Can vote up to expiration time.
419            TestSingleChoiceVote {
420                voter: "ezek".to_string(),
421                position: Vote::No,
422                weight: Uint128::new(10),
423                should_execute: ShouldExecute::Yes,
424            },
425        ],
426        Threshold::ThresholdQuorum {
427            threshold: PercentageThreshold::Percent(Decimal::percent(10)),
428            quorum: PercentageThreshold::Majority {},
429        },
430        Status::Passed,
431        None,
432    );
433}
434
435pub fn test_majority_vs_half<F>(do_votes: F)
436where
437    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
438{
439    do_votes(
440        vec![
441            TestSingleChoiceVote {
442                voter: "ekez".to_string(),
443                position: Vote::No,
444                weight: Uint128::new(10),
445                should_execute: ShouldExecute::Yes,
446            },
447            TestSingleChoiceVote {
448                voter: "keze".to_string(),
449                position: Vote::Yes,
450                weight: Uint128::new(10),
451                should_execute: ShouldExecute::Yes,
452            },
453        ],
454        Threshold::ThresholdQuorum {
455            threshold: PercentageThreshold::Percent(Decimal::percent(50)),
456            quorum: PercentageThreshold::Majority {},
457        },
458        Status::Passed,
459        None,
460    );
461
462    do_votes(
463        vec![
464            TestSingleChoiceVote {
465                voter: "ekez".to_string(),
466                position: Vote::No,
467                weight: Uint128::new(10),
468                should_execute: ShouldExecute::Yes,
469            },
470            // Can vote up to expiration time, even if it already rejected.
471            TestSingleChoiceVote {
472                voter: "keze".to_string(),
473                position: Vote::Yes,
474                weight: Uint128::new(10),
475                should_execute: ShouldExecute::Yes,
476            },
477        ],
478        Threshold::ThresholdQuorum {
479            threshold: PercentageThreshold::Majority {},
480            quorum: PercentageThreshold::Majority {},
481        },
482        Status::Rejected,
483        None,
484    );
485}
486
487pub fn test_pass_threshold_not_quorum<F>(do_votes: F)
488where
489    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
490{
491    do_votes(
492        vec![TestSingleChoiceVote {
493            voter: "ekez".to_string(),
494            position: Vote::Yes,
495            weight: Uint128::new(59),
496            should_execute: ShouldExecute::Yes,
497        }],
498        Threshold::ThresholdQuorum {
499            threshold: PercentageThreshold::Majority {},
500            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
501        },
502        Status::Open,
503        Some(Uint128::new(100)),
504    );
505    do_votes(
506        vec![TestSingleChoiceVote {
507            voter: "ekez".to_string(),
508            position: Vote::No,
509            weight: Uint128::new(59),
510            should_execute: ShouldExecute::Yes,
511        }],
512        Threshold::ThresholdQuorum {
513            threshold: PercentageThreshold::Majority {},
514            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
515        },
516        // As the threshold is 50% and 59% of voters have voted no
517        // this is unable to pass.
518        Status::Rejected,
519        Some(Uint128::new(100)),
520    );
521}
522
523pub fn test_pass_exactly_quorum<F>(do_votes: F)
524where
525    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
526{
527    do_votes(
528        vec![TestSingleChoiceVote {
529            voter: "ekez".to_string(),
530            position: Vote::Yes,
531            weight: Uint128::new(60),
532            should_execute: ShouldExecute::Yes,
533        }],
534        Threshold::ThresholdQuorum {
535            threshold: PercentageThreshold::Majority {},
536            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
537        },
538        Status::Passed,
539        Some(Uint128::new(100)),
540    );
541    do_votes(
542        vec![
543            TestSingleChoiceVote {
544                voter: "ekez".to_string(),
545                position: Vote::Yes,
546                weight: Uint128::new(59),
547                should_execute: ShouldExecute::Yes,
548            },
549            // This is an intersting one because in this case the no
550            // voter is actually incentivised not to vote. By voting
551            // they move the quorum over the threshold and pass the
552            // vote. In a DAO with sufficently involved stakeholders
553            // no voters should effectively never vote if there is a
554            // quorum higher than the threshold as it makes the
555            // passing threshold the quorum threshold.
556            TestSingleChoiceVote {
557                voter: "keze".to_string(),
558                position: Vote::No,
559                weight: Uint128::new(1),
560                should_execute: ShouldExecute::Yes,
561            },
562        ],
563        Threshold::ThresholdQuorum {
564            threshold: PercentageThreshold::Majority {},
565            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
566        },
567        Status::Passed,
568        Some(Uint128::new(100)),
569    );
570    do_votes(
571        vec![TestSingleChoiceVote {
572            voter: "ekez".to_string(),
573            position: Vote::No,
574            weight: Uint128::new(60),
575            should_execute: ShouldExecute::Yes,
576        }],
577        Threshold::ThresholdQuorum {
578            threshold: PercentageThreshold::Majority {},
579            quorum: PercentageThreshold::Percent(Decimal::percent(60)),
580        },
581        Status::Rejected,
582        Some(Uint128::new(100)),
583    );
584}
585
586pub fn fuzz_voting<F>(do_votes: F)
587where
588    F: Fn(Vec<TestSingleChoiceVote>, Threshold, Status, Option<Uint128>),
589{
590    let mut rng = rand::thread_rng();
591    let dist = rand::distributions::Uniform::<u64>::new(1, 200);
592    for _ in 0..10 {
593        let yes: Vec<u64> = (0..50).map(|_| rng.sample(dist)).collect();
594        let no: Vec<u64> = (0..50).map(|_| rng.sample(dist)).collect();
595
596        let yes_sum: u64 = yes.iter().sum();
597        let no_sum: u64 = no.iter().sum();
598        let expected_status = match yes_sum.cmp(&no_sum) {
599            std::cmp::Ordering::Less => Status::Rejected,
600            // Depends on which reaches the threshold first. Ignore for now.
601            std::cmp::Ordering::Equal => Status::Rejected,
602            std::cmp::Ordering::Greater => Status::Passed,
603        };
604
605        let yes = yes
606            .into_iter()
607            .enumerate()
608            .map(|(idx, weight)| TestSingleChoiceVote {
609                voter: format!("yes_{idx}"),
610                position: Vote::Yes,
611                weight: Uint128::new(weight as u128),
612                should_execute: ShouldExecute::Meh,
613            });
614        let no = no
615            .into_iter()
616            .enumerate()
617            .map(|(idx, weight)| TestSingleChoiceVote {
618                voter: format!("no_{idx}"),
619                position: Vote::No,
620                weight: Uint128::new(weight as u128),
621                should_execute: ShouldExecute::Meh,
622            });
623        let mut votes = yes.chain(no).collect::<Vec<_>>();
624        votes.shuffle(&mut rng);
625
626        do_votes(
627            votes,
628            Threshold::AbsolutePercentage {
629                percentage: PercentageThreshold::Majority {},
630            },
631            expected_status,
632            None,
633        );
634    }
635}