1use std::{collections::HashSet, fmt::Display};
47
48use cosmwasm_std::{
49 ensure_eq, Addr, BlockInfo, Decimal, StdError, StdResult, Storage, Timestamp, Uint128, Uint64,
50};
51use cw_storage_plus::{Bound, Item, Map};
52use thiserror::Error;
53
54#[derive(Error, Debug, PartialEq)]
55pub enum VoteError {
56 #[error("Std error encountered while handling voting object: {0}")]
57 Std(#[from] StdError),
58
59 #[error("Tried to add duplicate voter addresses")]
60 DuplicateAddrs {},
61
62 #[error("No proposal by proposal id")]
63 NoProposalById {},
64
65 #[error("Action allowed only for active proposal")]
66 ProposalNotActive(ProposalStatus),
67
68 #[error("Threshold error: {0}")]
69 ThresholdError(String),
70
71 #[error("Veto actions could be done only during veto period, current status: {status}")]
72 NotVeto { status: ProposalStatus },
73
74 #[error("Too early to count votes: voting is not over")]
75 VotingNotOver {},
76
77 #[error("User is not allowed to vote on this proposal")]
78 Unauthorized {},
79}
80
81pub type VoteResult<T> = Result<T, VoteError>;
82
83pub const DEFAULT_LIMIT: u64 = 25;
84pub type ProposalId = u64;
85
86pub struct SimpleVoting<'a> {
88 next_proposal_id: Item<'a, ProposalId>,
89 proposals: Map<'a, (ProposalId, &'a Addr), Option<Vote>>,
90 proposals_info: Map<'a, ProposalId, ProposalInfo>,
91 vote_config: Item<'a, VoteConfig>,
92}
93
94impl<'a> SimpleVoting<'a> {
95 pub const fn new(
96 proposals_key: &'a str,
97 id_key: &'a str,
98 proposals_info_key: &'a str,
99 vote_config_key: &'a str,
100 ) -> Self {
101 Self {
102 next_proposal_id: Item::new(id_key),
103 proposals: Map::new(proposals_key),
104 proposals_info: Map::new(proposals_info_key),
105 vote_config: Item::new(vote_config_key),
106 }
107 }
108
109 pub fn instantiate(&self, store: &mut dyn Storage, vote_config: &VoteConfig) -> VoteResult<()> {
111 vote_config.threshold.validate_percentage()?;
112
113 self.next_proposal_id.save(store, &ProposalId::default())?;
114 self.vote_config.save(store, vote_config)?;
115 Ok(())
116 }
117
118 pub fn update_vote_config(
119 &self,
120 store: &mut dyn Storage,
121 new_vote_config: &VoteConfig,
122 ) -> StdResult<()> {
123 self.vote_config.save(store, new_vote_config)
124 }
125
126 pub fn new_proposal(
129 &self,
130 store: &mut dyn Storage,
131 end: Timestamp,
132 initial_voters: &[Addr],
133 ) -> VoteResult<ProposalId> {
134 let mut unique_addrs = HashSet::with_capacity(initial_voters.len());
136 if !initial_voters.iter().all(|x| unique_addrs.insert(x)) {
137 return Err(VoteError::DuplicateAddrs {});
138 }
139
140 let proposal_id = self
141 .next_proposal_id
142 .update(store, |id| VoteResult::Ok(id + 1))?;
143
144 let config = self.load_config(store)?;
145 self.proposals_info.save(
146 store,
147 proposal_id,
148 &ProposalInfo::new(initial_voters.len() as u32, config, end),
149 )?;
150 for voter in initial_voters {
151 self.proposals.save(store, (proposal_id, voter), &None)?;
152 }
153 Ok(proposal_id)
154 }
155
156 pub fn cast_vote(
158 &self,
159 store: &mut dyn Storage,
160 block: &BlockInfo,
161 proposal_id: ProposalId,
162 voter: &Addr,
163 vote: Vote,
164 ) -> VoteResult<ProposalInfo> {
165 let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
166 proposal_info.assert_active_proposal()?;
167
168 self.proposals.update(
169 store,
170 (proposal_id, voter),
171 |previous_vote| match previous_vote {
172 Some(prev_v) => {
174 proposal_info.vote_update(prev_v.as_ref(), &vote);
175 Ok(Some(vote))
176 }
177 None => Err(VoteError::Unauthorized {}),
178 },
179 )?;
180
181 self.proposals_info
182 .save(store, proposal_id, &proposal_info)?;
183 Ok(proposal_info)
184 }
185
186 pub fn cancel_proposal(
190 &self,
191 store: &mut dyn Storage,
192 block: &BlockInfo,
193 proposal_id: ProposalId,
194 ) -> VoteResult<ProposalInfo> {
195 let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
196 proposal_info.assert_active_proposal()?;
197
198 proposal_info.finish_vote(ProposalOutcome::Canceled {}, block);
199 self.proposals_info
200 .save(store, proposal_id, &proposal_info)?;
201 Ok(proposal_info)
202 }
203
204 pub fn count_votes(
206 &self,
207 store: &mut dyn Storage,
208 block: &BlockInfo,
209 proposal_id: ProposalId,
210 ) -> VoteResult<(ProposalInfo, ProposalOutcome)> {
211 let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
212 ensure_eq!(
213 proposal_info.status,
214 ProposalStatus::WaitingForCount,
215 VoteError::VotingNotOver {}
216 );
217
218 let vote_config = &proposal_info.config;
219
220 let threshold = match vote_config.threshold {
222 Threshold::Majority {} => Uint128::from(proposal_info.total_voters / 2 + 1),
224 Threshold::Percentage(decimal) => decimal * Uint128::from(proposal_info.total_voters),
225 };
226
227 let proposal_outcome = if Uint128::from(proposal_info.votes_for) >= threshold {
228 ProposalOutcome::Passed
229 } else {
230 ProposalOutcome::Failed
231 };
232
233 proposal_info.finish_vote(proposal_outcome, block);
235 self.proposals_info
236 .save(store, proposal_id, &proposal_info)?;
237
238 Ok((proposal_info, proposal_outcome))
239 }
240
241 pub fn veto_proposal(
244 &self,
245 store: &mut dyn Storage,
246 block: &BlockInfo,
247 proposal_id: ProposalId,
248 ) -> VoteResult<ProposalInfo> {
249 let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
250
251 let ProposalStatus::VetoPeriod(_) = proposal_info.status else {
252 return Err(VoteError::NotVeto {
253 status: proposal_info.status,
254 });
255 };
256
257 proposal_info.status = ProposalStatus::Finished(ProposalOutcome::Vetoed);
258 self.proposals_info
259 .save(store, proposal_id, &proposal_info)?;
260
261 Ok(proposal_info)
262 }
263
264 pub fn load_vote(
327 &self,
328 store: &dyn Storage,
329 proposal_id: ProposalId,
330 voter: &Addr,
331 ) -> VoteResult<Option<Vote>> {
332 self.proposals
333 .load(store, (proposal_id, voter))
334 .map_err(Into::into)
335 }
336
337 pub fn load_proposal(
339 &self,
340 store: &dyn Storage,
341 block: &BlockInfo,
342 proposal_id: ProposalId,
343 ) -> VoteResult<ProposalInfo> {
344 let mut proposal = self
345 .proposals_info
346 .may_load(store, proposal_id)?
347 .ok_or(VoteError::NoProposalById {})?;
348 if let ProposalStatus::Active = proposal.status {
349 let veto_expiration = proposal.end_timestamp.plus_seconds(
350 proposal
351 .config
352 .veto_duration_seconds
353 .unwrap_or_default()
354 .u64(),
355 );
356 if block.time >= proposal.end_timestamp {
358 if block.time < veto_expiration {
359 proposal.status = ProposalStatus::VetoPeriod(veto_expiration)
360 } else {
361 proposal.status = ProposalStatus::WaitingForCount
362 }
363 }
364 }
365 Ok(proposal)
366 }
367
368 pub fn load_config(&self, store: &dyn Storage) -> StdResult<VoteConfig> {
370 self.vote_config.load(store)
371 }
372
373 pub fn query_by_id(
375 &self,
376 store: &dyn Storage,
377 proposal_id: ProposalId,
378 start_after: Option<&Addr>,
379 limit: Option<u64>,
380 ) -> VoteResult<Vec<(Addr, Option<Vote>)>> {
381 let min = start_after.map(Bound::exclusive);
382 let limit = limit.unwrap_or(DEFAULT_LIMIT);
383
384 let votes = self
385 .proposals
386 .prefix(proposal_id)
387 .range(store, min, None, cosmwasm_std::Order::Ascending)
388 .take(limit as usize)
389 .collect::<StdResult<_>>()?;
390 Ok(votes)
391 }
392
393 #[allow(clippy::type_complexity)]
394 pub fn query_list(
395 &self,
396 store: &dyn Storage,
397 start_after: Option<(ProposalId, &Addr)>,
398 limit: Option<u64>,
399 ) -> VoteResult<Vec<((ProposalId, Addr), Option<Vote>)>> {
400 let min = start_after.map(Bound::exclusive);
401 let limit = limit.unwrap_or(DEFAULT_LIMIT);
402
403 let votes = self
404 .proposals
405 .range(store, min, None, cosmwasm_std::Order::Ascending)
406 .take(limit as usize)
407 .collect::<StdResult<_>>()?;
408 Ok(votes)
409 }
410}
411
412#[cosmwasm_schema::cw_serde]
414pub struct Vote {
415 pub vote: bool,
418 pub memo: Option<String>,
420}
421
422#[cosmwasm_schema::cw_serde]
423pub struct ProposalInfo {
424 pub total_voters: u32,
425 pub votes_for: u32,
426 pub votes_against: u32,
427 pub status: ProposalStatus,
428 pub config: VoteConfig,
431 pub end_timestamp: Timestamp,
432}
433
434impl ProposalInfo {
435 pub fn new(initial_voters: u32, config: VoteConfig, end_timestamp: Timestamp) -> Self {
436 Self {
437 total_voters: initial_voters,
438 votes_for: 0,
439 votes_against: 0,
440 config,
441 status: ProposalStatus::Active {},
442 end_timestamp,
443 }
444 }
445
446 pub fn assert_active_proposal(&self) -> VoteResult<()> {
447 self.status.assert_is_active()
448 }
449
450 pub fn vote_update(&mut self, previous_vote: Option<&Vote>, new_vote: &Vote) {
451 match (previous_vote, new_vote.vote) {
452 (Some(Vote { vote: true, .. }), true) | (Some(Vote { vote: false, .. }), false) => {}
454 (Some(Vote { vote: true, .. }), false) => {
456 self.votes_against += 1;
457 self.votes_for -= 1;
458 }
459 (Some(Vote { vote: false, .. }), true) => {
461 self.votes_for += 1;
462 self.votes_against -= 1;
463 }
464 (None, true) => {
466 self.votes_for += 1;
467 }
468 (None, false) => {
470 self.votes_against += 1;
471 }
472 }
473 }
474
475 pub fn finish_vote(&mut self, outcome: ProposalOutcome, block: &BlockInfo) {
476 self.status = ProposalStatus::Finished(outcome);
477 self.end_timestamp = block.time
478 }
479}
480
481#[cosmwasm_schema::cw_serde]
482pub enum ProposalStatus {
483 Active,
484 VetoPeriod(Timestamp),
485 WaitingForCount,
486 Finished(ProposalOutcome),
487}
488
489impl Display for ProposalStatus {
490 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
491 match self {
492 ProposalStatus::Active => write!(f, "active"),
493 ProposalStatus::VetoPeriod(exp) => write!(f, "veto_period until {exp}"),
494 ProposalStatus::WaitingForCount => write!(f, "waiting_for_count"),
495 ProposalStatus::Finished(outcome) => write!(f, "finished({outcome})"),
496 }
497 }
498}
499
500impl ProposalStatus {
501 pub fn assert_is_active(&self) -> VoteResult<()> {
502 match self {
503 ProposalStatus::Active => Ok(()),
504 _ => Err(VoteError::ProposalNotActive(self.clone())),
505 }
506 }
507}
508
509#[cosmwasm_schema::cw_serde]
510#[derive(Copy)]
511pub enum ProposalOutcome {
512 Passed,
513 Failed,
514 Canceled,
515 Vetoed,
516}
517
518impl Display for ProposalOutcome {
519 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
520 match self {
521 ProposalOutcome::Passed => write!(f, "passed"),
522 ProposalOutcome::Failed => write!(f, "failed"),
523 ProposalOutcome::Canceled => write!(f, "canceled"),
524 ProposalOutcome::Vetoed => write!(f, "vetoed"),
525 }
526 }
527}
528
529#[cosmwasm_schema::cw_serde]
530pub struct VoteConfig {
531 pub threshold: Threshold,
532 pub veto_duration_seconds: Option<Uint64>,
535}
536
537#[cosmwasm_schema::cw_serde]
538pub enum Threshold {
539 Majority {},
540 Percentage(Decimal),
541}
542
543impl Threshold {
544 fn validate_percentage(&self) -> VoteResult<()> {
546 if let Threshold::Percentage(percent) = self {
547 if percent.is_zero() {
548 Err(VoteError::ThresholdError("can't be 0%".to_owned()))
549 } else if *percent > Decimal::one() {
550 Err(VoteError::ThresholdError(
551 "not possible to reach >100% votes".to_owned(),
552 ))
553 } else {
554 Ok(())
555 }
556 } else {
557 Ok(())
558 }
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use cosmwasm_std::testing::{mock_dependencies, mock_env};
565
566 use super::*;
567 const SIMPLE_VOTING: SimpleVoting =
568 SimpleVoting::new("proposals", "id", "proposal_info", "config");
569
570 fn setup(storage: &mut dyn Storage, vote_config: &VoteConfig) {
571 SIMPLE_VOTING.instantiate(storage, vote_config).unwrap();
572 }
573 fn default_setup(storage: &mut dyn Storage) {
574 setup(
575 storage,
576 &VoteConfig {
577 threshold: Threshold::Majority {},
578 veto_duration_seconds: None,
579 },
580 );
581 }
582
583 #[test]
584 fn threshold_validation() {
585 assert!(Threshold::Majority {}.validate_percentage().is_ok());
586 assert!(Threshold::Percentage(Decimal::one())
587 .validate_percentage()
588 .is_ok());
589 assert!(Threshold::Percentage(Decimal::percent(1))
590 .validate_percentage()
591 .is_ok());
592
593 assert_eq!(
594 Threshold::Percentage(Decimal::percent(101)).validate_percentage(),
595 Err(VoteError::ThresholdError(
596 "not possible to reach >100% votes".to_owned()
597 ))
598 );
599 assert_eq!(
600 Threshold::Percentage(Decimal::zero()).validate_percentage(),
601 Err(VoteError::ThresholdError("can't be 0%".to_owned()))
602 );
603 }
604
605 #[test]
606 fn assert_active_proposal() {
607 let end_timestamp = Timestamp::from_seconds(100);
608
609 let mut proposal = ProposalInfo {
611 total_voters: 2,
612 votes_for: 0,
613 votes_against: 0,
614 status: ProposalStatus::Active,
615 config: VoteConfig {
616 threshold: Threshold::Majority {},
617 veto_duration_seconds: Some(Uint64::new(10)),
618 },
619 end_timestamp,
620 };
621 assert!(proposal.assert_active_proposal().is_ok());
622
623 proposal.status = ProposalStatus::VetoPeriod(end_timestamp.plus_seconds(10));
625 assert_eq!(
626 proposal.assert_active_proposal().unwrap_err(),
627 VoteError::ProposalNotActive(ProposalStatus::VetoPeriod(
628 end_timestamp.plus_seconds(10)
629 ))
630 );
631 }
632
633 #[test]
634 fn create_proposal() {
635 let mut deps = mock_dependencies();
636 let env = mock_env();
637 let storage = &mut deps.storage;
638 default_setup(storage);
639
640 let end_timestamp = env.block.time.plus_seconds(100);
641 let proposal_id = SIMPLE_VOTING
643 .new_proposal(
644 storage,
645 end_timestamp,
646 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
647 )
648 .unwrap();
649 assert_eq!(proposal_id, 1);
650
651 let proposal = SIMPLE_VOTING
652 .load_proposal(storage, &env.block, proposal_id)
653 .unwrap();
654 assert_eq!(
655 proposal,
656 ProposalInfo {
657 total_voters: 2,
658 votes_for: 0,
659 votes_against: 0,
660 status: ProposalStatus::Active,
661 config: VoteConfig {
662 threshold: Threshold::Majority {},
663 veto_duration_seconds: None
664 },
665 end_timestamp
666 }
667 );
668
669 let proposal_id = SIMPLE_VOTING
671 .new_proposal(
672 storage,
673 env.block.time,
674 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
675 )
676 .unwrap();
677 assert_eq!(proposal_id, 2);
678
679 let proposal = SIMPLE_VOTING
680 .load_proposal(storage, &env.block, proposal_id)
681 .unwrap();
682 assert_eq!(
683 proposal,
684 ProposalInfo {
685 total_voters: 2,
686 votes_for: 0,
687 votes_against: 0,
688 status: ProposalStatus::WaitingForCount,
689 config: VoteConfig {
690 threshold: Threshold::Majority {},
691 veto_duration_seconds: None
692 },
693 end_timestamp: env.block.time
694 }
695 );
696 }
697
698 #[test]
699 fn create_proposal_duplicate_friends() {
700 let mut deps = mock_dependencies();
701 let env = mock_env();
702 let storage = &mut deps.storage;
703 default_setup(storage);
704
705 let end_timestamp = env.block.time.plus_seconds(100);
706
707 let err = SIMPLE_VOTING
708 .new_proposal(
709 storage,
710 end_timestamp,
711 &[Addr::unchecked("alice"), Addr::unchecked("alice")],
712 )
713 .unwrap_err();
714 assert_eq!(err, VoteError::DuplicateAddrs {});
715 }
716
717 #[test]
718 fn cancel_vote() {
719 let mut deps = mock_dependencies();
720 let env = mock_env();
721 let storage = &mut deps.storage;
722
723 default_setup(storage);
724
725 let end_timestamp = env.block.time.plus_seconds(100);
726 let proposal_id = SIMPLE_VOTING
728 .new_proposal(
729 storage,
730 end_timestamp,
731 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
732 )
733 .unwrap();
734
735 SIMPLE_VOTING
736 .cancel_proposal(storage, &env.block, proposal_id)
737 .unwrap();
738
739 let proposal = SIMPLE_VOTING
740 .load_proposal(storage, &env.block, proposal_id)
741 .unwrap();
742 assert_eq!(
743 proposal,
744 ProposalInfo {
745 total_voters: 2,
746 votes_for: 0,
747 votes_against: 0,
748 status: ProposalStatus::Finished(ProposalOutcome::Canceled),
749 config: VoteConfig {
750 threshold: Threshold::Majority {},
751 veto_duration_seconds: None
752 },
753 end_timestamp: env.block.time
755 }
756 );
757
758 let err = SIMPLE_VOTING
760 .cancel_proposal(storage, &env.block, proposal_id)
761 .unwrap_err();
762 assert_eq!(
763 err,
764 VoteError::ProposalNotActive(ProposalStatus::Finished(ProposalOutcome::Canceled))
765 );
766 }
767
768 #[test]
770 fn load_proposal() {
771 let mut deps = mock_dependencies();
772 let mut env = mock_env();
773 let storage = &mut deps.storage;
774 setup(
775 storage,
776 &VoteConfig {
777 threshold: Threshold::Majority {},
778 veto_duration_seconds: Some(Uint64::new(10)),
779 },
780 );
781
782 let end_timestamp = env.block.time.plus_seconds(100);
783 let proposal_id = SIMPLE_VOTING
784 .new_proposal(storage, end_timestamp, &[Addr::unchecked("alice")])
785 .unwrap();
786 let proposal: ProposalInfo = SIMPLE_VOTING
787 .load_proposal(storage, &env.block, proposal_id)
788 .unwrap();
789 assert_eq!(proposal.status, ProposalStatus::Active,);
790
791 env.block.time = end_timestamp;
793 let proposal: ProposalInfo = SIMPLE_VOTING
794 .load_proposal(storage, &env.block, proposal_id)
795 .unwrap();
796 assert_eq!(
797 proposal.status,
798 ProposalStatus::VetoPeriod(end_timestamp.plus_seconds(10)),
799 );
800
801 env.block.time = end_timestamp.plus_seconds(10);
803 let proposal: ProposalInfo = SIMPLE_VOTING
804 .load_proposal(storage, &env.block, proposal_id)
805 .unwrap();
806 assert_eq!(proposal.status, ProposalStatus::WaitingForCount,);
807
808 SIMPLE_VOTING
810 .count_votes(storage, &env.block, proposal_id)
811 .unwrap();
812 let proposal: ProposalInfo = SIMPLE_VOTING
813 .load_proposal(storage, &env.block, proposal_id)
814 .unwrap();
815 assert!(matches!(proposal.status, ProposalStatus::Finished(_)));
816
817 SIMPLE_VOTING
818 .update_vote_config(
819 storage,
820 &VoteConfig {
821 threshold: Threshold::Majority {},
822 veto_duration_seconds: None,
823 },
824 )
825 .unwrap();
826
827 let end_timestamp = env.block.time.plus_seconds(100);
828 let proposal_id = SIMPLE_VOTING
829 .new_proposal(storage, end_timestamp, &[Addr::unchecked("alice")])
830 .unwrap();
831 env.block.time = end_timestamp;
833 let proposal: ProposalInfo = SIMPLE_VOTING
834 .load_proposal(storage, &env.block, proposal_id)
835 .unwrap();
836 assert_eq!(proposal.status, ProposalStatus::WaitingForCount,);
837 }
838
839 #[test]
840 fn cast_vote() {
841 let mut deps = mock_dependencies();
842 let env = mock_env();
843 let storage = &mut deps.storage;
844 default_setup(storage);
845
846 let end_timestamp = env.block.time.plus_seconds(100);
847 let proposal_id = SIMPLE_VOTING
848 .new_proposal(
849 storage,
850 end_timestamp,
851 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
852 )
853 .unwrap();
854
855 SIMPLE_VOTING
857 .cast_vote(
858 deps.as_mut().storage,
859 &env.block,
860 proposal_id,
861 &Addr::unchecked("alice"),
862 Vote {
863 vote: false,
864 memo: None,
865 },
866 )
867 .unwrap();
868 let vote = SIMPLE_VOTING
869 .load_vote(
870 deps.as_ref().storage,
871 proposal_id,
872 &Addr::unchecked("alice"),
873 )
874 .unwrap()
875 .unwrap();
876 assert_eq!(
877 vote,
878 Vote {
879 vote: false,
880 memo: None
881 }
882 );
883 let proposal = SIMPLE_VOTING
884 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
885 .unwrap();
886 assert_eq!(
887 proposal,
888 ProposalInfo {
889 total_voters: 2,
890 votes_for: 0,
891 votes_against: 1,
892 status: ProposalStatus::Active,
893 config: VoteConfig {
894 threshold: Threshold::Majority {},
895 veto_duration_seconds: None
896 },
897 end_timestamp
898 }
899 );
900
901 SIMPLE_VOTING
903 .cast_vote(
904 deps.as_mut().storage,
905 &env.block,
906 proposal_id,
907 &Addr::unchecked("bob"),
908 Vote {
909 vote: false,
910 memo: Some("memo".to_owned()),
911 },
912 )
913 .unwrap();
914 let vote = SIMPLE_VOTING
915 .load_vote(deps.as_ref().storage, proposal_id, &Addr::unchecked("bob"))
916 .unwrap()
917 .unwrap();
918 assert_eq!(
919 vote,
920 Vote {
921 vote: false,
922 memo: Some("memo".to_owned())
923 }
924 );
925 let proposal = SIMPLE_VOTING
926 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
927 .unwrap();
928 assert_eq!(
929 proposal,
930 ProposalInfo {
931 total_voters: 2,
932 votes_for: 0,
933 votes_against: 2,
934 status: ProposalStatus::Active,
935 config: VoteConfig {
936 threshold: Threshold::Majority {},
937 veto_duration_seconds: None
938 },
939 end_timestamp
940 }
941 );
942
943 SIMPLE_VOTING
945 .cast_vote(
946 deps.as_mut().storage,
947 &env.block,
948 proposal_id,
949 &Addr::unchecked("alice"),
950 Vote {
951 vote: false,
952 memo: None,
953 },
954 )
955 .unwrap();
956 let proposal = SIMPLE_VOTING
958 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
959 .unwrap();
960 assert_eq!(
961 proposal,
962 ProposalInfo {
963 total_voters: 2,
964 votes_for: 0,
965 votes_against: 2,
966 status: ProposalStatus::Active,
967 config: VoteConfig {
968 threshold: Threshold::Majority {},
969 veto_duration_seconds: None
970 },
971 end_timestamp
972 }
973 );
974
975 SIMPLE_VOTING
977 .cast_vote(
978 deps.as_mut().storage,
979 &env.block,
980 proposal_id,
981 &Addr::unchecked("bob"),
982 Vote {
983 vote: true,
984 memo: None,
985 },
986 )
987 .unwrap();
988 let proposal = SIMPLE_VOTING
990 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
991 .unwrap();
992 assert_eq!(
993 proposal,
994 ProposalInfo {
995 total_voters: 2,
996 votes_for: 1,
997 votes_against: 1,
998 status: ProposalStatus::Active,
999 config: VoteConfig {
1000 threshold: Threshold::Majority {},
1001 veto_duration_seconds: None
1002 },
1003 end_timestamp
1004 }
1005 );
1006 }
1007
1008 #[test]
1009 fn invalid_cast_votes() {
1010 let mut deps = mock_dependencies();
1011 let mut env = mock_env();
1012 let storage = &mut deps.storage;
1013 setup(
1014 storage,
1015 &VoteConfig {
1016 threshold: Threshold::Majority {},
1017 veto_duration_seconds: Some(Uint64::new(10)),
1018 },
1019 );
1020
1021 let end_timestamp = env.block.time.plus_seconds(100);
1022 let proposal_id = SIMPLE_VOTING
1023 .new_proposal(
1024 storage,
1025 end_timestamp,
1026 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
1027 )
1028 .unwrap();
1029
1030 let err = SIMPLE_VOTING
1032 .cast_vote(
1033 deps.as_mut().storage,
1034 &env.block,
1035 proposal_id,
1036 &Addr::unchecked("stranger"),
1037 Vote {
1038 vote: false,
1039 memo: None,
1040 },
1041 )
1042 .unwrap_err();
1043 assert_eq!(err, VoteError::Unauthorized {});
1044
1045 env.block.time = end_timestamp;
1047
1048 let err = SIMPLE_VOTING
1050 .cast_vote(
1051 deps.as_mut().storage,
1052 &env.block,
1053 proposal_id,
1054 &Addr::unchecked("alice"),
1055 Vote {
1056 vote: false,
1057 memo: None,
1058 },
1059 )
1060 .unwrap_err();
1061 assert_eq!(
1062 err,
1063 VoteError::ProposalNotActive(ProposalStatus::VetoPeriod(
1064 env.block.time.plus_seconds(10)
1065 ))
1066 );
1067
1068 env.block.time = env.block.time.plus_seconds(10);
1069
1070 let err = SIMPLE_VOTING
1072 .cast_vote(
1073 deps.as_mut().storage,
1074 &env.block,
1075 proposal_id,
1076 &Addr::unchecked("alice"),
1077 Vote {
1078 vote: false,
1079 memo: None,
1080 },
1081 )
1082 .unwrap_err();
1083 assert_eq!(
1084 err,
1085 VoteError::ProposalNotActive(ProposalStatus::WaitingForCount)
1086 );
1087
1088 SIMPLE_VOTING
1090 .count_votes(deps.as_mut().storage, &env.block, proposal_id)
1091 .unwrap();
1092 let err = SIMPLE_VOTING
1093 .cast_vote(
1094 deps.as_mut().storage,
1095 &env.block,
1096 proposal_id,
1097 &Addr::unchecked("alice"),
1098 Vote {
1099 vote: false,
1100 memo: None,
1101 },
1102 )
1103 .unwrap_err();
1104 assert_eq!(
1105 err,
1106 VoteError::ProposalNotActive(ProposalStatus::Finished(ProposalOutcome::Failed))
1107 );
1108 }
1109
1110 #[test]
1111 fn count_votes() {
1112 let mut deps = mock_dependencies();
1113 let mut env = mock_env();
1114 let storage = &mut deps.storage;
1115 default_setup(storage);
1116
1117 let end_timestamp = env.block.time.plus_seconds(100);
1119 let proposal_id = SIMPLE_VOTING
1120 .new_proposal(
1121 storage,
1122 end_timestamp,
1123 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
1124 )
1125 .unwrap();
1126 env.block.time = end_timestamp.plus_seconds(10);
1127 SIMPLE_VOTING
1128 .count_votes(storage, &env.block, proposal_id)
1129 .unwrap();
1130 let proposal = SIMPLE_VOTING
1131 .load_proposal(storage, &env.block, proposal_id)
1132 .unwrap();
1133 assert_eq!(
1134 proposal,
1135 ProposalInfo {
1136 total_voters: 2,
1137 votes_for: 0,
1138 votes_against: 0,
1139 status: ProposalStatus::Finished(ProposalOutcome::Failed),
1140 config: VoteConfig {
1141 threshold: Threshold::Majority {},
1142 veto_duration_seconds: None
1143 },
1144 end_timestamp: end_timestamp.plus_seconds(10)
1145 }
1146 );
1147
1148 let end_timestamp = env.block.time.plus_seconds(100);
1150 let proposal_id = SIMPLE_VOTING
1151 .new_proposal(
1152 storage,
1153 end_timestamp,
1154 &[
1155 Addr::unchecked("alice"),
1156 Addr::unchecked("bob"),
1157 Addr::unchecked("afk"),
1158 ],
1159 )
1160 .unwrap();
1161 SIMPLE_VOTING
1162 .cast_vote(
1163 storage,
1164 &env.block,
1165 proposal_id,
1166 &Addr::unchecked("alice"),
1167 Vote {
1168 vote: true,
1169 memo: None,
1170 },
1171 )
1172 .unwrap();
1173 SIMPLE_VOTING
1174 .cast_vote(
1175 storage,
1176 &env.block,
1177 proposal_id,
1178 &Addr::unchecked("bob"),
1179 Vote {
1180 vote: true,
1181 memo: None,
1182 },
1183 )
1184 .unwrap();
1185 env.block.time = end_timestamp;
1186 SIMPLE_VOTING
1187 .count_votes(storage, &env.block, proposal_id)
1188 .unwrap();
1189 let proposal = SIMPLE_VOTING
1190 .load_proposal(storage, &env.block, proposal_id)
1191 .unwrap();
1192 assert_eq!(
1193 proposal,
1194 ProposalInfo {
1195 total_voters: 3,
1196 votes_for: 2,
1197 votes_against: 0,
1198 status: ProposalStatus::Finished(ProposalOutcome::Passed),
1199 config: VoteConfig {
1200 threshold: Threshold::Majority {},
1201 veto_duration_seconds: None
1202 },
1203 end_timestamp
1204 }
1205 );
1206
1207 SIMPLE_VOTING
1209 .update_vote_config(
1210 storage,
1211 &VoteConfig {
1212 threshold: Threshold::Percentage(Decimal::percent(50)),
1213 veto_duration_seconds: None,
1214 },
1215 )
1216 .unwrap();
1217 let end_timestamp = env.block.time.plus_seconds(100);
1218 let proposal_id = SIMPLE_VOTING
1219 .new_proposal(
1220 storage,
1221 end_timestamp,
1222 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
1223 )
1224 .unwrap();
1225 SIMPLE_VOTING
1226 .cast_vote(
1227 storage,
1228 &env.block,
1229 proposal_id,
1230 &Addr::unchecked("alice"),
1231 Vote {
1232 vote: true,
1233 memo: None,
1234 },
1235 )
1236 .unwrap();
1237
1238 env.block.time = end_timestamp;
1239 SIMPLE_VOTING
1240 .count_votes(storage, &env.block, proposal_id)
1241 .unwrap();
1242 let proposal = SIMPLE_VOTING
1243 .load_proposal(storage, &env.block, proposal_id)
1244 .unwrap();
1245 assert_eq!(
1246 proposal,
1247 ProposalInfo {
1248 total_voters: 2,
1249 votes_for: 1,
1250 votes_against: 0,
1251 status: ProposalStatus::Finished(ProposalOutcome::Passed),
1252 config: VoteConfig {
1253 threshold: Threshold::Percentage(Decimal::percent(50)),
1254 veto_duration_seconds: None
1255 },
1256 end_timestamp
1257 }
1258 );
1259 }
1260}