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 pub title: String,
21 pub description: String,
23 pub proposer: Addr,
25 pub start_height: u64,
29 pub min_voting_period: Option<Expiration>,
33 pub expiration: Expiration,
36 pub choices: Vec<CheckedMultipleChoiceOption>,
38 pub status: Status,
40 pub voting_strategy: VotingStrategy,
42 pub total_power: Uint128,
44 pub votes: MultipleChoiceVotes,
46 pub allow_revoting: bool,
51 pub veto: Option<VetoConfig>,
54}
55
56pub enum VoteResult {
57 SingleWinner(CheckedMultipleChoiceOption),
58 Tie,
59}
60
61impl MultipleChoiceProposal {
62 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 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 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 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 expiration.is_expired(block) {
99 Ok(Status::Passed)
100 } else {
101 Ok(self.status)
102 }
103 }
104 _ => Ok(self.status),
105 }
106 }
107
108 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 pub fn is_passed(&self, block: &BlockInfo) -> StdResult<bool> {
122 if self.allow_revoting && !self.expiration.is_expired(block) {
125 return Ok(false);
126 }
127 if let Some(min) = self.min_voting_period {
133 if !min.is_expired(block) {
134 return Ok(false);
135 }
136 }
137
138 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 VoteResult::Tie => return Ok(false),
148 VoteResult::SingleWinner(winning_choice) => {
149 if winning_choice.option_type != MultipleChoiceOptionType::None {
151 if self.expiration.is_expired(block) {
153 return Ok(true);
154 } else {
155 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 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 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 (true, true) => {
193 if winning_choice.option_type == MultipleChoiceOptionType::None {
195 return Ok(true);
196 }
197 Ok(false)
198 }
199 (true, false) | (false, false) => {
201 if winning_choice.option_type == MultipleChoiceOptionType::None {
204 return self.is_choice_unbeatable(&winning_choice);
205 }
206 Ok(false)
207 }
208 (false, true) => Ok(true),
210 }
211 }
212 }
213 }
214
215 pub fn calculate_vote_result(&self) -> StdResult<VoteResult> {
217 match self.voting_strategy {
218 VotingStrategy::SingleChoice { quorum: _ } => {
219 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 assert!(prop.is_passed(&env.block).unwrap());
601 assert!(!prop.is_rejected(&env.block).unwrap());
602
603 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 assert!(!prop.is_passed(&env.block).unwrap());
624 assert!(prop.is_rejected(&env.block).unwrap());
625
626 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 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 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 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 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 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 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 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 assert!(prop.is_passed(&env.block).unwrap());
758 }
759
760 #[test]
761 fn test_majority_revote_rejection() {
762 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 assert_eq!(prop.total_power, prop.votes.total());
782 assert_eq!(prop.votes.vote_weights[0], prop.votes.vote_weights[1]);
783 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 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 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 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 assert!(prop.is_passed(&env.block).unwrap());
835 }
836
837 #[test]
838 fn test_percentage_revote_rejection() {
839 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 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 assert!(prop.is_rejected(&env.block).unwrap());
877 }
878}