dao_proposal_multiple/
proposal.rs

1use std::ops::Add;
2
3use cosmwasm_schema::cw_serde;
4use cosmwasm_std::{Addr, BlockInfo, StdError, StdResult, Uint128};
5use cw_utils::Expiration;
6use dao_voting::{
7    multiple_choice::{
8        CheckedMultipleChoiceOption, MultipleChoiceOptionType, MultipleChoiceVotes, VotingStrategy,
9    },
10    status::Status,
11    veto::VetoConfig,
12    voting::does_vote_count_pass,
13};
14
15use crate::query::ProposalResponse;
16
17#[cw_serde]
18pub struct MultipleChoiceProposal {
19    /// The title of the proposal
20    pub title: String,
21    /// The main body of the proposal text
22    pub description: String,
23    /// The address that created this proposal.
24    pub proposer: Addr,
25    /// The block height at which this proposal was created. Voting
26    /// power queries should query for voting power at this block
27    /// height.
28    pub start_height: u64,
29    /// The minimum amount of time this proposal must remain open for
30    /// voting. The proposal may not pass unless this is expired or
31    /// None.
32    pub min_voting_period: Option<Expiration>,
33    /// The the time at which this proposal will expire and close for
34    /// additional votes.
35    pub expiration: Expiration,
36    /// The options to be chosen from in the vote.
37    pub choices: Vec<CheckedMultipleChoiceOption>,
38    /// The proposal status
39    pub status: Status,
40    /// Voting settings (threshold, quorum, etc.)
41    pub voting_strategy: VotingStrategy,
42    /// The total power when the proposal started (used to calculate percentages)
43    pub total_power: Uint128,
44    /// The vote tally.
45    pub votes: MultipleChoiceVotes,
46    /// Whether DAO members are allowed to change their votes.
47    /// When disabled, proposals can be executed as soon as they pass.
48    /// When enabled, proposals can only be executed after the voting
49    /// perid has ended and the proposal passed.
50    pub allow_revoting: bool,
51    /// Optional veto configuration. If set to `None`, veto option
52    /// is disabled. Otherwise contains the configuration for veto flow.
53    pub veto: Option<VetoConfig>,
54}
55
56pub enum VoteResult {
57    SingleWinner(CheckedMultipleChoiceOption),
58    Tie,
59}
60
61impl MultipleChoiceProposal {
62    /// Consumes the proposal and returns a version which may be used
63    /// in a query response. The difference being that proposal
64    /// statuses are only updated on vote, execute, and close
65    /// events. It is possible though that since a vote has occured
66    /// the proposal expiring has changed its status. This method
67    /// recomputes the status so that queries get accurate
68    /// information.
69    pub fn into_response(mut self, block: &BlockInfo, id: u64) -> StdResult<ProposalResponse> {
70        self.update_status(block)?;
71        Ok(ProposalResponse { id, proposal: self })
72    }
73
74    /// Gets the current status of the proposal.
75    pub fn current_status(&self, block: &BlockInfo) -> StdResult<Status> {
76        match self.status {
77            Status::Open if self.is_passed(block)? => match &self.veto {
78                // if prop is passed and veto is configured, calculate timelock
79                // expiration. if it's expired, this proposal has passed.
80                // otherwise, set status to `VetoTimelock`.
81                Some(veto_config) => {
82                    let expiration = self.expiration.add(veto_config.timelock_duration)?;
83
84                    if expiration.is_expired(block) {
85                        Ok(Status::Passed)
86                    } else {
87                        Ok(Status::VetoTimelock { expiration })
88                    }
89                }
90                // Otherwise the proposal is simply passed
91                None => Ok(Status::Passed),
92            },
93            Status::Open if self.expiration.is_expired(block) || self.is_rejected(block)? => {
94                Ok(Status::Rejected)
95            }
96            Status::VetoTimelock { expiration } => {
97                // if prop timelock expired, proposal is now passed.
98                if expiration.is_expired(block) {
99                    Ok(Status::Passed)
100                } else {
101                    Ok(self.status)
102                }
103            }
104            _ => Ok(self.status),
105        }
106    }
107
108    /// Sets a proposals status to its current status.
109    pub fn update_status(&mut self, block: &BlockInfo) -> StdResult<()> {
110        let new_status = self.current_status(block)?;
111        self.status = new_status;
112        Ok(())
113    }
114
115    /// Returns true iff this proposal is sure to pass (even before
116    /// expiration if no future sequence of possible votes can cause
117    /// it to fail). Passing in the case of multiple choice proposals
118    /// means that quorum has been met,
119    /// one of the options that is not "None of the above"
120    /// has won the most votes, and there is no tie.
121    pub fn is_passed(&self, block: &BlockInfo) -> StdResult<bool> {
122        // If re-voting is allowed nothing is known until the proposal
123        // has expired.
124        if self.allow_revoting && !self.expiration.is_expired(block) {
125            return Ok(false);
126        }
127        // If the min voting period is set and not expired the
128        // proposal can not yet be passed. This gives DAO members some
129        // time to remove liquidity / scheme on a recovery plan if a
130        // single actor accumulates enough tokens to unilaterally pass
131        // proposals.
132        if let Some(min) = self.min_voting_period {
133            if !min.is_expired(block) {
134                return Ok(false);
135            }
136        }
137
138        // Proposal can only pass if quorum has been met.
139        if does_vote_count_pass(
140            self.votes.total(),
141            self.total_power,
142            self.voting_strategy.get_quorum(),
143        ) {
144            let vote_result = self.calculate_vote_result()?;
145            match vote_result {
146                // Proposal is not passed if there is a tie.
147                VoteResult::Tie => return Ok(false),
148                VoteResult::SingleWinner(winning_choice) => {
149                    // Proposal is not passed if winning choice is None.
150                    if winning_choice.option_type != MultipleChoiceOptionType::None {
151                        // If proposal is expired, quorum has been reached, and winning choice is neither tied nor None, then proposal is passed.
152                        if self.expiration.is_expired(block) {
153                            return Ok(true);
154                        } else {
155                            // If the proposal is not expired but the leading choice cannot
156                            // possibly be outwon by any other choices, the proposal has passed.
157                            return self.is_choice_unbeatable(&winning_choice);
158                        }
159                    }
160                }
161            }
162        }
163        Ok(false)
164    }
165
166    pub fn is_rejected(&self, block: &BlockInfo) -> StdResult<bool> {
167        // If re-voting is allowed and the proposal is not expired no
168        // information is known.
169        if self.allow_revoting && !self.expiration.is_expired(block) {
170            return Ok(false);
171        }
172
173        let vote_result = self.calculate_vote_result()?;
174        match vote_result {
175            // Proposal is rejected if there is a tie, and either the proposal is expired or
176            // there is no voting power left.
177            VoteResult::Tie => {
178                let rejected =
179                    self.expiration.is_expired(block) || self.total_power == self.votes.total();
180                Ok(rejected)
181            }
182            VoteResult::SingleWinner(winning_choice) => {
183                match (
184                    does_vote_count_pass(
185                        self.votes.total(),
186                        self.total_power,
187                        self.voting_strategy.get_quorum(),
188                    ),
189                    self.expiration.is_expired(block),
190                ) {
191                    // Quorum is met and proposal is expired.
192                    (true, true) => {
193                        // Proposal is rejected if "None" is the winning option.
194                        if winning_choice.option_type == MultipleChoiceOptionType::None {
195                            return Ok(true);
196                        }
197                        Ok(false)
198                    }
199                    // Proposal is not expired, quorum is either is met or unmet.
200                    (true, false) | (false, false) => {
201                        // If the proposal is not expired and the leading choice is None and it cannot
202                        // possibly be outwon by any other choices, the proposal is rejected.
203                        if winning_choice.option_type == MultipleChoiceOptionType::None {
204                            return self.is_choice_unbeatable(&winning_choice);
205                        }
206                        Ok(false)
207                    }
208                    // Quorum is not met and proposal is expired.
209                    (false, true) => Ok(true),
210                }
211            }
212        }
213    }
214
215    /// Find the option with the highest vote weight, and note if there is a tie.
216    pub fn calculate_vote_result(&self) -> StdResult<VoteResult> {
217        match self.voting_strategy {
218            VotingStrategy::SingleChoice { quorum: _ } => {
219                // We expect to have at least 3 vote weights
220                if let Some(max_weight) = self.votes.vote_weights.iter().max_by(|&a, &b| a.cmp(b)) {
221                    let top_choices: Vec<(usize, &Uint128)> = self
222                        .votes
223                        .vote_weights
224                        .iter()
225                        .enumerate()
226                        .filter(|x| x.1 == max_weight)
227                        .collect();
228
229                    // If more than one choice has the highest number of votes, we have a tie.
230                    if top_choices.len() > 1 {
231                        return Ok(VoteResult::Tie);
232                    }
233
234                    match top_choices.first() {
235                        Some(winning_choice) => {
236                            return Ok(VoteResult::SingleWinner(
237                                self.choices[winning_choice.0].clone(),
238                            ));
239                        }
240                        None => {
241                            return Err(StdError::generic_err("no votes found"));
242                        }
243                    }
244                }
245                Err(StdError::not_found("max vote weight"))
246            }
247        }
248    }
249
250    /// Ensure that with the remaining vote power, the choice with the second highest votes
251    /// cannot overtake the first choice.
252    fn is_choice_unbeatable(
253        &self,
254        winning_choice: &CheckedMultipleChoiceOption,
255    ) -> StdResult<bool> {
256        let winning_choice_power = self.votes.vote_weights[winning_choice.index as usize];
257        if let Some(second_choice_power) = self
258            .votes
259            .vote_weights
260            .iter()
261            .filter(|&x| x < &winning_choice_power)
262            .max_by(|&a, &b| a.cmp(b))
263        {
264            // Check if the remaining vote power can be used to overtake the current winning choice.
265            let remaining_vote_power = self.total_power - self.votes.total();
266            match winning_choice.option_type {
267                MultipleChoiceOptionType::Standard => {
268                    if winning_choice_power > *second_choice_power + remaining_vote_power {
269                        return Ok(true);
270                    }
271                }
272                MultipleChoiceOptionType::None => {
273                    // If the winning choice is None, and we can at most achieve a tie,
274                    // this choice is unbeatable because a tie will also fail the proposal. This is why we check for '>=' in this case
275                    // rather than '>'.
276                    if winning_choice_power >= *second_choice_power + remaining_vote_power {
277                        return Ok(true);
278                    }
279                }
280            }
281        } else {
282            return Err(StdError::not_found("second highest vote weight"));
283        }
284        Ok(false)
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    use cosmwasm_std::testing::mock_env;
293    use dao_voting::multiple_choice::{MultipleChoiceOption, MultipleChoiceOptions};
294
295    fn create_proposal(
296        block: &BlockInfo,
297        voting_strategy: VotingStrategy,
298        votes: MultipleChoiceVotes,
299        total_power: Uint128,
300        is_expired: bool,
301        allow_revoting: bool,
302    ) -> MultipleChoiceProposal {
303        // The last option that gets added in into_checked is always the none of the above option
304        let options = vec![
305            MultipleChoiceOption {
306                description: "multiple choice option 1".to_string(),
307                msgs: vec![],
308                title: "title".to_string(),
309            },
310            MultipleChoiceOption {
311                description: "multiple choice option 2".to_string(),
312                msgs: vec![],
313                title: "title".to_string(),
314            },
315        ];
316
317        let expiration: Expiration = if is_expired {
318            Expiration::AtHeight(block.height - 5)
319        } else {
320            Expiration::AtHeight(block.height + 5)
321        };
322
323        let mc_options = MultipleChoiceOptions { options };
324        MultipleChoiceProposal {
325            title: "A simple text proposal".to_string(),
326            description: "A simple text proposal".to_string(),
327            proposer: Addr::unchecked("CREATOR"),
328            start_height: mock_env().block.height,
329            expiration,
330            // The last option that gets added in into_checked is always the none of the above option
331            choices: mc_options.into_checked().unwrap().options,
332            status: Status::Open,
333            voting_strategy,
334            total_power,
335            votes,
336            allow_revoting,
337            min_voting_period: None,
338            veto: None,
339        }
340    }
341
342    #[test]
343    fn test_majority_quorum() {
344        let env = mock_env();
345        let voting_strategy = VotingStrategy::SingleChoice {
346            quorum: dao_voting::threshold::PercentageThreshold::Majority {},
347        };
348
349        let votes = MultipleChoiceVotes {
350            vote_weights: vec![Uint128::new(1), Uint128::new(0), Uint128::new(0)],
351        };
352
353        let prop = create_proposal(
354            &env.block,
355            voting_strategy.clone(),
356            votes,
357            Uint128::new(1),
358            false,
359            false,
360        );
361
362        // Quorum was met and all votes were cast, should be passed.
363        assert!(prop.is_passed(&env.block).unwrap());
364        assert!(!prop.is_rejected(&env.block).unwrap());
365
366        let votes = MultipleChoiceVotes {
367            vote_weights: vec![Uint128::new(0), Uint128::new(0), Uint128::new(1)],
368        };
369        let prop = create_proposal(
370            &env.block,
371            voting_strategy.clone(),
372            votes,
373            Uint128::new(1),
374            false,
375            false,
376        );
377
378        // Quorum was met but none of the above won, should be rejected.
379        assert!(!prop.is_passed(&env.block).unwrap());
380        assert!(prop.is_rejected(&env.block).unwrap());
381
382        let votes = MultipleChoiceVotes {
383            vote_weights: vec![Uint128::new(1), Uint128::new(0), Uint128::new(0)],
384        };
385        let prop = create_proposal(
386            &env.block,
387            voting_strategy.clone(),
388            votes,
389            Uint128::new(100),
390            false,
391            false,
392        );
393
394        // Quorum was not met and is not expired, should be open.
395        assert!(!prop.is_passed(&env.block).unwrap());
396        assert!(!prop.is_rejected(&env.block).unwrap());
397
398        let votes = MultipleChoiceVotes {
399            vote_weights: vec![Uint128::new(1), Uint128::new(0), Uint128::new(0)],
400        };
401        let prop = create_proposal(
402            &env.block,
403            voting_strategy.clone(),
404            votes,
405            Uint128::new(100),
406            true,
407            false,
408        );
409
410        // Quorum was not met and it is expired, should be rejected.
411        assert!(!prop.is_passed(&env.block).unwrap());
412        assert!(prop.is_rejected(&env.block).unwrap());
413
414        let votes = MultipleChoiceVotes {
415            vote_weights: vec![Uint128::new(50), Uint128::new(50), Uint128::new(0)],
416        };
417        let prop = create_proposal(
418            &env.block,
419            voting_strategy.clone(),
420            votes,
421            Uint128::new(100),
422            true,
423            false,
424        );
425
426        // Quorum was met but it is a tie and expired, should be rejected.
427        assert!(!prop.is_passed(&env.block).unwrap());
428        assert!(prop.is_rejected(&env.block).unwrap());
429
430        let votes = MultipleChoiceVotes {
431            vote_weights: vec![Uint128::new(50), Uint128::new(50), Uint128::new(0)],
432        };
433        let prop = create_proposal(
434            &env.block,
435            voting_strategy,
436            votes,
437            Uint128::new(150),
438            false,
439            false,
440        );
441
442        // Quorum was met but it is a tie but not expired and still voting power remains, should be open.
443        assert!(!prop.is_passed(&env.block).unwrap());
444        assert!(!prop.is_rejected(&env.block).unwrap());
445    }
446
447    #[test]
448    fn test_percentage_quorum() {
449        let env = mock_env();
450        let voting_strategy = VotingStrategy::SingleChoice {
451            quorum: dao_voting::threshold::PercentageThreshold::Percent(
452                cosmwasm_std::Decimal::percent(10),
453            ),
454        };
455
456        let votes = MultipleChoiceVotes {
457            vote_weights: vec![Uint128::new(1), Uint128::new(0), Uint128::new(0)],
458        };
459
460        let prop = create_proposal(
461            &env.block,
462            voting_strategy.clone(),
463            votes,
464            Uint128::new(1),
465            false,
466            false,
467        );
468
469        // Quorum was met and all votes were cast, should be passed.
470        assert!(prop.is_passed(&env.block).unwrap());
471        assert!(!prop.is_rejected(&env.block).unwrap());
472
473        let votes = MultipleChoiceVotes {
474            vote_weights: vec![Uint128::new(0), Uint128::new(0), Uint128::new(1)],
475        };
476        let prop = create_proposal(
477            &env.block,
478            voting_strategy.clone(),
479            votes,
480            Uint128::new(1),
481            false,
482            false,
483        );
484
485        // Quorum was met but none of the above won, should be rejected.
486        assert!(!prop.is_passed(&env.block).unwrap());
487        assert!(prop.is_rejected(&env.block).unwrap());
488
489        let votes = MultipleChoiceVotes {
490            vote_weights: vec![Uint128::new(1), Uint128::new(0), Uint128::new(0)],
491        };
492        let prop = create_proposal(
493            &env.block,
494            voting_strategy.clone(),
495            votes,
496            Uint128::new(100),
497            false,
498            false,
499        );
500
501        // Quorum was not met and is not expired, should be open.
502        assert!(!prop.is_passed(&env.block).unwrap());
503        assert!(!prop.is_rejected(&env.block).unwrap());
504
505        let votes = MultipleChoiceVotes {
506            vote_weights: vec![Uint128::new(1), Uint128::new(0), Uint128::new(0)],
507        };
508        let prop = create_proposal(
509            &env.block,
510            voting_strategy.clone(),
511            votes,
512            Uint128::new(101),
513            true,
514            false,
515        );
516
517        // Quorum was not met and it is expired, should be rejected.
518        assert!(!prop.is_passed(&env.block).unwrap());
519        assert!(prop.is_rejected(&env.block).unwrap());
520
521        let votes = MultipleChoiceVotes {
522            vote_weights: vec![Uint128::new(50), Uint128::new(50), Uint128::new(0)],
523        };
524        let prop = create_proposal(
525            &env.block,
526            voting_strategy.clone(),
527            votes,
528            Uint128::new(10000),
529            true,
530            false,
531        );
532
533        // Quorum was met but it is a tie and expired, should be rejected.
534        assert!(!prop.is_passed(&env.block).unwrap());
535        assert!(prop.is_rejected(&env.block).unwrap());
536
537        let votes = MultipleChoiceVotes {
538            vote_weights: vec![Uint128::new(50), Uint128::new(50), Uint128::new(0)],
539        };
540        let prop = create_proposal(
541            &env.block,
542            voting_strategy,
543            votes,
544            Uint128::new(150),
545            false,
546            false,
547        );
548
549        // Quorum was met but it is a tie but not expired and still voting power remains, should be open.
550        assert!(!prop.is_passed(&env.block).unwrap());
551        assert!(!prop.is_rejected(&env.block).unwrap());
552    }
553
554    #[test]
555    fn test_unbeatable_none_option() {
556        let env = mock_env();
557        let voting_strategy = VotingStrategy::SingleChoice {
558            quorum: dao_voting::threshold::PercentageThreshold::Percent(
559                cosmwasm_std::Decimal::percent(10),
560            ),
561        };
562        let votes = MultipleChoiceVotes {
563            vote_weights: vec![Uint128::new(0), Uint128::new(50), Uint128::new(500)],
564        };
565        let prop = create_proposal(
566            &env.block,
567            voting_strategy,
568            votes,
569            Uint128::new(1000),
570            false,
571            false,
572        );
573
574        // Quorum was met but none of the above is winning, but it also can't be beat (only a tie at best), should be rejected
575        assert!(!prop.is_passed(&env.block).unwrap());
576        assert!(prop.is_rejected(&env.block).unwrap());
577    }
578
579    #[test]
580    fn test_quorum_rounding() {
581        let env = mock_env();
582        let voting_strategy = VotingStrategy::SingleChoice {
583            quorum: dao_voting::threshold::PercentageThreshold::Percent(
584                cosmwasm_std::Decimal::percent(10),
585            ),
586        };
587        let votes = MultipleChoiceVotes {
588            vote_weights: vec![Uint128::new(10), Uint128::new(0), Uint128::new(0)],
589        };
590        let prop = create_proposal(
591            &env.block,
592            voting_strategy,
593            votes,
594            Uint128::new(100),
595            true,
596            false,
597        );
598
599        // Quorum was met and proposal expired, should pass
600        assert!(prop.is_passed(&env.block).unwrap());
601        assert!(!prop.is_rejected(&env.block).unwrap());
602
603        // High Precision rounding
604        let voting_strategy = VotingStrategy::SingleChoice {
605            quorum: dao_voting::threshold::PercentageThreshold::Percent(
606                cosmwasm_std::Decimal::percent(100),
607            ),
608        };
609
610        let votes = MultipleChoiceVotes {
611            vote_weights: vec![Uint128::new(999999), Uint128::new(0), Uint128::new(0)],
612        };
613        let prop = create_proposal(
614            &env.block,
615            voting_strategy,
616            votes,
617            Uint128::new(1000000),
618            true,
619            false,
620        );
621
622        // Quorum was not met and expired, should reject
623        assert!(!prop.is_passed(&env.block).unwrap());
624        assert!(prop.is_rejected(&env.block).unwrap());
625
626        // High Precision rounding
627        let voting_strategy = VotingStrategy::SingleChoice {
628            quorum: dao_voting::threshold::PercentageThreshold::Percent(
629                cosmwasm_std::Decimal::percent(99),
630            ),
631        };
632
633        let votes = MultipleChoiceVotes {
634            vote_weights: vec![Uint128::new(9888889), Uint128::new(0), Uint128::new(0)],
635        };
636        let prop = create_proposal(
637            &env.block,
638            voting_strategy,
639            votes,
640            Uint128::new(10000000),
641            true,
642            false,
643        );
644
645        // Quorum was not met and expired, should reject
646        assert!(!prop.is_passed(&env.block).unwrap());
647        assert!(prop.is_rejected(&env.block).unwrap());
648    }
649
650    #[test]
651    fn test_tricky_pass() {
652        let env = mock_env();
653        let voting_strategy = VotingStrategy::SingleChoice {
654            quorum: dao_voting::threshold::PercentageThreshold::Percent(
655                cosmwasm_std::Decimal::from_ratio(7u32, 13u32),
656            ),
657        };
658        let votes = MultipleChoiceVotes {
659            vote_weights: vec![Uint128::new(7), Uint128::new(0), Uint128::new(6)],
660        };
661        let prop = create_proposal(
662            &env.block,
663            voting_strategy.clone(),
664            votes.clone(),
665            Uint128::new(13),
666            true,
667            false,
668        );
669
670        // Should pass if expired
671        assert!(prop.is_passed(&env.block).unwrap());
672        assert!(!prop.is_rejected(&env.block).unwrap());
673
674        let prop = create_proposal(
675            &env.block,
676            voting_strategy,
677            votes,
678            Uint128::new(13),
679            false,
680            false,
681        );
682
683        // Should pass if not expired
684        assert!(prop.is_passed(&env.block).unwrap());
685        assert!(!prop.is_rejected(&env.block).unwrap());
686    }
687
688    #[test]
689    fn test_tricky_pass_majority() {
690        let env = mock_env();
691        let voting_strategy = VotingStrategy::SingleChoice {
692            quorum: dao_voting::threshold::PercentageThreshold::Majority {},
693        };
694
695        let votes = MultipleChoiceVotes {
696            vote_weights: vec![Uint128::new(7), Uint128::new(0), Uint128::new(0)],
697        };
698        let prop = create_proposal(
699            &env.block,
700            voting_strategy.clone(),
701            votes.clone(),
702            Uint128::new(13),
703            true,
704            false,
705        );
706
707        // Should pass if majority voted
708        assert!(prop.is_passed(&env.block).unwrap());
709        assert!(!prop.is_rejected(&env.block).unwrap());
710
711        let prop = create_proposal(
712            &env.block,
713            voting_strategy,
714            votes,
715            Uint128::new(14),
716            true,
717            false,
718        );
719
720        // Shouldn't pass if only half voted
721        assert!(!prop.is_passed(&env.block).unwrap());
722        assert!(prop.is_rejected(&env.block).unwrap());
723    }
724
725    #[test]
726    fn test_majority_revote_pass() {
727        // Revoting being allowed means that proposals may not be
728        // passed or rejected before they expire.
729        let env = mock_env();
730        let voting_strategy = VotingStrategy::SingleChoice {
731            quorum: dao_voting::threshold::PercentageThreshold::Majority {},
732        };
733        let votes = MultipleChoiceVotes {
734            vote_weights: vec![Uint128::new(6), Uint128::new(0), Uint128::new(0)],
735        };
736
737        let prop = create_proposal(
738            &env.block,
739            voting_strategy.clone(),
740            votes.clone(),
741            Uint128::new(10),
742            false,
743            true,
744        );
745        // Quorum reached, but proposal is still active => no pass
746        assert!(!prop.is_passed(&env.block).unwrap());
747
748        let prop = create_proposal(
749            &env.block,
750            voting_strategy,
751            votes,
752            Uint128::new(10),
753            true,
754            true,
755        );
756        // Quorum reached & proposal has expired => pass
757        assert!(prop.is_passed(&env.block).unwrap());
758    }
759
760    #[test]
761    fn test_majority_revote_rejection() {
762        // Revoting being allowed means that proposals may not be
763        // passed or rejected before they expire.
764        let env = mock_env();
765        let voting_strategy = VotingStrategy::SingleChoice {
766            quorum: dao_voting::threshold::PercentageThreshold::Majority {},
767        };
768        let votes = MultipleChoiceVotes {
769            vote_weights: vec![Uint128::new(5), Uint128::new(5), Uint128::new(0)],
770        };
771
772        let prop = create_proposal(
773            &env.block,
774            voting_strategy.clone(),
775            votes.clone(),
776            Uint128::new(10),
777            false,
778            true,
779        );
780        // Everyone voted and proposal is in a tie...
781        assert_eq!(prop.total_power, prop.votes.total());
782        assert_eq!(prop.votes.vote_weights[0], prop.votes.vote_weights[1]);
783        // ... but proposal is still active => no rejection
784        assert!(!prop.is_rejected(&env.block).unwrap());
785
786        let prop = create_proposal(
787            &env.block,
788            voting_strategy,
789            votes,
790            Uint128::new(10),
791            true,
792            true,
793        );
794        // Proposal has expired and ended in a tie => rejection
795        assert_eq!(prop.votes.vote_weights[0], prop.votes.vote_weights[1]);
796        assert!(prop.is_rejected(&env.block).unwrap());
797    }
798
799    #[test]
800    fn test_percentage_revote_pass() {
801        // Revoting being allowed means that proposals may not be
802        // passed or rejected before they expire.
803        let env = mock_env();
804        let voting_strategy = VotingStrategy::SingleChoice {
805            quorum: dao_voting::threshold::PercentageThreshold::Percent(
806                cosmwasm_std::Decimal::percent(80),
807            ),
808        };
809
810        let votes = MultipleChoiceVotes {
811            vote_weights: vec![Uint128::new(81), Uint128::new(0), Uint128::new(0)],
812        };
813
814        let prop = create_proposal(
815            &env.block,
816            voting_strategy.clone(),
817            votes.clone(),
818            Uint128::new(100),
819            false,
820            true,
821        );
822        // Quorum reached, but proposal is still active => no pass
823        assert!(!prop.is_passed(&env.block).unwrap());
824
825        let prop = create_proposal(
826            &env.block,
827            voting_strategy,
828            votes,
829            Uint128::new(100),
830            true,
831            true,
832        );
833        // Quorum reached & proposal has expired => pass
834        assert!(prop.is_passed(&env.block).unwrap());
835    }
836
837    #[test]
838    fn test_percentage_revote_rejection() {
839        // Revoting being allowed means that proposals may not be
840        // passed or rejected before they expire.
841        let env = mock_env();
842        let voting_strategy = VotingStrategy::SingleChoice {
843            quorum: dao_voting::threshold::PercentageThreshold::Percent(
844                cosmwasm_std::Decimal::percent(80),
845            ),
846        };
847
848        let votes = MultipleChoiceVotes {
849            vote_weights: vec![Uint128::new(90), Uint128::new(0), Uint128::new(0)],
850        };
851
852        let prop = create_proposal(
853            &env.block,
854            voting_strategy.clone(),
855            votes,
856            Uint128::new(100),
857            false,
858            true,
859        );
860        // Quorum reached, but proposal is still active => no rejection
861        assert!(!prop.is_rejected(&env.block).unwrap());
862
863        let votes = MultipleChoiceVotes {
864            vote_weights: vec![Uint128::new(50), Uint128::new(0), Uint128::new(0)],
865        };
866
867        let prop = create_proposal(
868            &env.block,
869            voting_strategy,
870            votes,
871            Uint128::new(100),
872            true,
873            true,
874        );
875        // No quorum reached & proposal has expired => rejection
876        assert!(prop.is_rejected(&env.block).unwrap());
877    }
878}