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<ProposalId>,
89 proposals: Map<(ProposalId, &'a Addr), Option<Vote>>,
90 proposals_info: Map<ProposalId, ProposalInfo>,
91 vote_config: Item<VoteConfig>,
92}
93
94impl SimpleVoting<'_> {
95 pub const fn new(
96 proposals_key: &'static str,
97 id_key: &'static str,
98 proposals_info_key: &'static str,
99 vote_config_key: &'static 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) => {
225 Uint128::from(proposal_info.total_voters).mul_floor(decimal)
226 }
227 };
228
229 let proposal_outcome = if Uint128::from(proposal_info.votes_for) >= threshold {
230 ProposalOutcome::Passed
231 } else {
232 ProposalOutcome::Failed
233 };
234
235 proposal_info.finish_vote(proposal_outcome, block);
237 self.proposals_info
238 .save(store, proposal_id, &proposal_info)?;
239
240 Ok((proposal_info, proposal_outcome))
241 }
242
243 pub fn veto_proposal(
246 &self,
247 store: &mut dyn Storage,
248 block: &BlockInfo,
249 proposal_id: ProposalId,
250 ) -> VoteResult<ProposalInfo> {
251 let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
252
253 let ProposalStatus::VetoPeriod(_) = proposal_info.status else {
254 return Err(VoteError::NotVeto {
255 status: proposal_info.status,
256 });
257 };
258
259 proposal_info.status = ProposalStatus::Finished(ProposalOutcome::Vetoed);
260 self.proposals_info
261 .save(store, proposal_id, &proposal_info)?;
262
263 Ok(proposal_info)
264 }
265
266 pub fn load_vote(
268 &self,
269 store: &dyn Storage,
270 proposal_id: ProposalId,
271 voter: &Addr,
272 ) -> VoteResult<Option<Vote>> {
273 self.proposals
274 .load(store, (proposal_id, voter))
275 .map_err(Into::into)
276 }
277
278 pub fn load_proposal(
280 &self,
281 store: &dyn Storage,
282 block: &BlockInfo,
283 proposal_id: ProposalId,
284 ) -> VoteResult<ProposalInfo> {
285 let mut proposal = self
286 .proposals_info
287 .may_load(store, proposal_id)?
288 .ok_or(VoteError::NoProposalById {})?;
289 if let ProposalStatus::Active = proposal.status {
290 let veto_expiration = proposal.end_timestamp.plus_seconds(
291 proposal
292 .config
293 .veto_duration_seconds
294 .unwrap_or_default()
295 .u64(),
296 );
297 if block.time >= proposal.end_timestamp {
299 if block.time < veto_expiration {
300 proposal.status = ProposalStatus::VetoPeriod(veto_expiration)
301 } else {
302 proposal.status = ProposalStatus::WaitingForCount
303 }
304 }
305 }
306 Ok(proposal)
307 }
308
309 pub fn load_config(&self, store: &dyn Storage) -> StdResult<VoteConfig> {
311 self.vote_config.load(store)
312 }
313
314 pub fn query_by_id(
316 &self,
317 store: &dyn Storage,
318 proposal_id: ProposalId,
319 start_after: Option<&Addr>,
320 limit: Option<u64>,
321 ) -> VoteResult<Vec<(Addr, Option<Vote>)>> {
322 let min = start_after.map(Bound::exclusive);
323 let limit = limit.unwrap_or(DEFAULT_LIMIT);
324
325 let votes = self
326 .proposals
327 .prefix(proposal_id)
328 .range(store, min, None, cosmwasm_std::Order::Ascending)
329 .take(limit as usize)
330 .collect::<StdResult<_>>()?;
331 Ok(votes)
332 }
333
334 #[allow(clippy::type_complexity)]
335 pub fn query_list(
336 &self,
337 store: &dyn Storage,
338 start_after: Option<(ProposalId, &Addr)>,
339 limit: Option<u64>,
340 ) -> VoteResult<Vec<((ProposalId, Addr), Option<Vote>)>> {
341 let min = start_after.map(Bound::exclusive);
342 let limit = limit.unwrap_or(DEFAULT_LIMIT);
343
344 let votes = self
345 .proposals
346 .range(store, min, None, cosmwasm_std::Order::Ascending)
347 .take(limit as usize)
348 .collect::<StdResult<_>>()?;
349 Ok(votes)
350 }
351}
352
353#[cosmwasm_schema::cw_serde]
355pub struct Vote {
356 pub vote: bool,
359 pub memo: Option<String>,
361}
362
363#[cosmwasm_schema::cw_serde]
364pub struct ProposalInfo {
365 pub total_voters: u32,
366 pub votes_for: u32,
367 pub votes_against: u32,
368 pub status: ProposalStatus,
369 pub config: VoteConfig,
372 pub end_timestamp: Timestamp,
373}
374
375impl ProposalInfo {
376 pub fn new(initial_voters: u32, config: VoteConfig, end_timestamp: Timestamp) -> Self {
377 Self {
378 total_voters: initial_voters,
379 votes_for: 0,
380 votes_against: 0,
381 config,
382 status: ProposalStatus::Active {},
383 end_timestamp,
384 }
385 }
386
387 pub fn assert_active_proposal(&self) -> VoteResult<()> {
388 self.status.assert_is_active()
389 }
390
391 pub fn vote_update(&mut self, previous_vote: Option<&Vote>, new_vote: &Vote) {
392 match (previous_vote, new_vote.vote) {
393 (Some(Vote { vote: true, .. }), true) | (Some(Vote { vote: false, .. }), false) => {}
395 (Some(Vote { vote: true, .. }), false) => {
397 self.votes_against += 1;
398 self.votes_for -= 1;
399 }
400 (Some(Vote { vote: false, .. }), true) => {
402 self.votes_for += 1;
403 self.votes_against -= 1;
404 }
405 (None, true) => {
407 self.votes_for += 1;
408 }
409 (None, false) => {
411 self.votes_against += 1;
412 }
413 }
414 }
415
416 pub fn finish_vote(&mut self, outcome: ProposalOutcome, block: &BlockInfo) {
417 self.status = ProposalStatus::Finished(outcome);
418 self.end_timestamp = block.time
419 }
420}
421
422#[cosmwasm_schema::cw_serde]
423pub enum ProposalStatus {
424 Active,
425 VetoPeriod(Timestamp),
426 WaitingForCount,
427 Finished(ProposalOutcome),
428}
429
430impl Display for ProposalStatus {
431 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
432 match self {
433 ProposalStatus::Active => write!(f, "active"),
434 ProposalStatus::VetoPeriod(exp) => write!(f, "veto_period until {exp}"),
435 ProposalStatus::WaitingForCount => write!(f, "waiting_for_count"),
436 ProposalStatus::Finished(outcome) => write!(f, "finished({outcome})"),
437 }
438 }
439}
440
441impl ProposalStatus {
442 pub fn assert_is_active(&self) -> VoteResult<()> {
443 match self {
444 ProposalStatus::Active => Ok(()),
445 _ => Err(VoteError::ProposalNotActive(self.clone())),
446 }
447 }
448}
449
450#[cosmwasm_schema::cw_serde]
451#[derive(Copy)]
452pub enum ProposalOutcome {
453 Passed,
454 Failed,
455 Canceled,
456 Vetoed,
457}
458
459impl Display for ProposalOutcome {
460 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 match self {
462 ProposalOutcome::Passed => write!(f, "passed"),
463 ProposalOutcome::Failed => write!(f, "failed"),
464 ProposalOutcome::Canceled => write!(f, "canceled"),
465 ProposalOutcome::Vetoed => write!(f, "vetoed"),
466 }
467 }
468}
469
470#[cosmwasm_schema::cw_serde]
471pub struct VoteConfig {
472 pub threshold: Threshold,
473 pub veto_duration_seconds: Option<Uint64>,
476}
477
478#[cosmwasm_schema::cw_serde]
479pub enum Threshold {
480 Majority {},
481 Percentage(Decimal),
482}
483
484impl Threshold {
485 fn validate_percentage(&self) -> VoteResult<()> {
487 if let Threshold::Percentage(percent) = self {
488 if percent.is_zero() {
489 Err(VoteError::ThresholdError("can't be 0%".to_owned()))
490 } else if *percent > Decimal::one() {
491 Err(VoteError::ThresholdError(
492 "not possible to reach >100% votes".to_owned(),
493 ))
494 } else {
495 Ok(())
496 }
497 } else {
498 Ok(())
499 }
500 }
501}
502
503#[cfg(test)]
504mod tests {
505 use cosmwasm_std::testing::{mock_dependencies, mock_env};
506
507 use super::*;
508 const SIMPLE_VOTING: SimpleVoting =
509 SimpleVoting::new("proposals", "id", "proposal_info", "config");
510
511 fn setup(storage: &mut dyn Storage, vote_config: &VoteConfig) {
512 SIMPLE_VOTING.instantiate(storage, vote_config).unwrap();
513 }
514 fn default_setup(storage: &mut dyn Storage) {
515 setup(
516 storage,
517 &VoteConfig {
518 threshold: Threshold::Majority {},
519 veto_duration_seconds: None,
520 },
521 );
522 }
523
524 #[coverage_helper::test]
525 fn threshold_validation() {
526 assert!(Threshold::Majority {}.validate_percentage().is_ok());
527 assert!(Threshold::Percentage(Decimal::one())
528 .validate_percentage()
529 .is_ok());
530 assert!(Threshold::Percentage(Decimal::percent(1))
531 .validate_percentage()
532 .is_ok());
533
534 assert_eq!(
535 Threshold::Percentage(Decimal::percent(101)).validate_percentage(),
536 Err(VoteError::ThresholdError(
537 "not possible to reach >100% votes".to_owned()
538 ))
539 );
540 assert_eq!(
541 Threshold::Percentage(Decimal::zero()).validate_percentage(),
542 Err(VoteError::ThresholdError("can't be 0%".to_owned()))
543 );
544 }
545
546 #[coverage_helper::test]
547 fn assert_active_proposal() {
548 let end_timestamp = Timestamp::from_seconds(100);
549
550 let mut proposal = ProposalInfo {
552 total_voters: 2,
553 votes_for: 0,
554 votes_against: 0,
555 status: ProposalStatus::Active,
556 config: VoteConfig {
557 threshold: Threshold::Majority {},
558 veto_duration_seconds: Some(Uint64::new(10)),
559 },
560 end_timestamp,
561 };
562 assert!(proposal.assert_active_proposal().is_ok());
563
564 proposal.status = ProposalStatus::VetoPeriod(end_timestamp.plus_seconds(10));
566 assert_eq!(
567 proposal.assert_active_proposal().unwrap_err(),
568 VoteError::ProposalNotActive(ProposalStatus::VetoPeriod(
569 end_timestamp.plus_seconds(10)
570 ))
571 );
572 }
573
574 #[coverage_helper::test]
575 fn create_proposal() {
576 let mut deps = mock_dependencies();
577 let env = mock_env();
578 let storage = &mut deps.storage;
579 default_setup(storage);
580
581 let end_timestamp = env.block.time.plus_seconds(100);
582 let proposal_id = SIMPLE_VOTING
584 .new_proposal(
585 storage,
586 end_timestamp,
587 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
588 )
589 .unwrap();
590 assert_eq!(proposal_id, 1);
591
592 let proposal = SIMPLE_VOTING
593 .load_proposal(storage, &env.block, proposal_id)
594 .unwrap();
595 assert_eq!(
596 proposal,
597 ProposalInfo {
598 total_voters: 2,
599 votes_for: 0,
600 votes_against: 0,
601 status: ProposalStatus::Active,
602 config: VoteConfig {
603 threshold: Threshold::Majority {},
604 veto_duration_seconds: None
605 },
606 end_timestamp
607 }
608 );
609
610 let proposal_id = SIMPLE_VOTING
612 .new_proposal(
613 storage,
614 env.block.time,
615 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
616 )
617 .unwrap();
618 assert_eq!(proposal_id, 2);
619
620 let proposal = SIMPLE_VOTING
621 .load_proposal(storage, &env.block, proposal_id)
622 .unwrap();
623 assert_eq!(
624 proposal,
625 ProposalInfo {
626 total_voters: 2,
627 votes_for: 0,
628 votes_against: 0,
629 status: ProposalStatus::WaitingForCount,
630 config: VoteConfig {
631 threshold: Threshold::Majority {},
632 veto_duration_seconds: None
633 },
634 end_timestamp: env.block.time
635 }
636 );
637 }
638
639 #[coverage_helper::test]
640 fn create_proposal_duplicate_friends() {
641 let mut deps = mock_dependencies();
642 let env = mock_env();
643 let storage = &mut deps.storage;
644 default_setup(storage);
645
646 let end_timestamp = env.block.time.plus_seconds(100);
647
648 let err = SIMPLE_VOTING
649 .new_proposal(
650 storage,
651 end_timestamp,
652 &[Addr::unchecked("alice"), Addr::unchecked("alice")],
653 )
654 .unwrap_err();
655 assert_eq!(err, VoteError::DuplicateAddrs {});
656 }
657
658 #[coverage_helper::test]
659 fn cancel_vote() {
660 let mut deps = mock_dependencies();
661 let env = mock_env();
662 let storage = &mut deps.storage;
663
664 default_setup(storage);
665
666 let end_timestamp = env.block.time.plus_seconds(100);
667 let proposal_id = SIMPLE_VOTING
669 .new_proposal(
670 storage,
671 end_timestamp,
672 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
673 )
674 .unwrap();
675
676 SIMPLE_VOTING
677 .cancel_proposal(storage, &env.block, proposal_id)
678 .unwrap();
679
680 let proposal = SIMPLE_VOTING
681 .load_proposal(storage, &env.block, proposal_id)
682 .unwrap();
683 assert_eq!(
684 proposal,
685 ProposalInfo {
686 total_voters: 2,
687 votes_for: 0,
688 votes_against: 0,
689 status: ProposalStatus::Finished(ProposalOutcome::Canceled),
690 config: VoteConfig {
691 threshold: Threshold::Majority {},
692 veto_duration_seconds: None
693 },
694 end_timestamp: env.block.time
696 }
697 );
698
699 let err = SIMPLE_VOTING
701 .cancel_proposal(storage, &env.block, proposal_id)
702 .unwrap_err();
703 assert_eq!(
704 err,
705 VoteError::ProposalNotActive(ProposalStatus::Finished(ProposalOutcome::Canceled))
706 );
707 }
708
709 #[coverage_helper::test]
711 fn load_proposal() {
712 let mut deps = mock_dependencies();
713 let mut env = mock_env();
714 let storage = &mut deps.storage;
715 setup(
716 storage,
717 &VoteConfig {
718 threshold: Threshold::Majority {},
719 veto_duration_seconds: Some(Uint64::new(10)),
720 },
721 );
722
723 let end_timestamp = env.block.time.plus_seconds(100);
724 let proposal_id = SIMPLE_VOTING
725 .new_proposal(storage, end_timestamp, &[Addr::unchecked("alice")])
726 .unwrap();
727 let proposal: ProposalInfo = SIMPLE_VOTING
728 .load_proposal(storage, &env.block, proposal_id)
729 .unwrap();
730 assert_eq!(proposal.status, ProposalStatus::Active,);
731
732 env.block.time = end_timestamp;
734 let proposal: ProposalInfo = SIMPLE_VOTING
735 .load_proposal(storage, &env.block, proposal_id)
736 .unwrap();
737 assert_eq!(
738 proposal.status,
739 ProposalStatus::VetoPeriod(end_timestamp.plus_seconds(10)),
740 );
741
742 env.block.time = end_timestamp.plus_seconds(10);
744 let proposal: ProposalInfo = SIMPLE_VOTING
745 .load_proposal(storage, &env.block, proposal_id)
746 .unwrap();
747 assert_eq!(proposal.status, ProposalStatus::WaitingForCount,);
748
749 SIMPLE_VOTING
751 .count_votes(storage, &env.block, proposal_id)
752 .unwrap();
753 let proposal: ProposalInfo = SIMPLE_VOTING
754 .load_proposal(storage, &env.block, proposal_id)
755 .unwrap();
756 assert!(matches!(proposal.status, ProposalStatus::Finished(_)));
757
758 SIMPLE_VOTING
759 .update_vote_config(
760 storage,
761 &VoteConfig {
762 threshold: Threshold::Majority {},
763 veto_duration_seconds: None,
764 },
765 )
766 .unwrap();
767
768 let end_timestamp = env.block.time.plus_seconds(100);
769 let proposal_id = SIMPLE_VOTING
770 .new_proposal(storage, end_timestamp, &[Addr::unchecked("alice")])
771 .unwrap();
772 env.block.time = end_timestamp;
774 let proposal: ProposalInfo = SIMPLE_VOTING
775 .load_proposal(storage, &env.block, proposal_id)
776 .unwrap();
777 assert_eq!(proposal.status, ProposalStatus::WaitingForCount,);
778 }
779
780 #[coverage_helper::test]
781 fn cast_vote() {
782 let mut deps = mock_dependencies();
783 let env = mock_env();
784 let storage = &mut deps.storage;
785 default_setup(storage);
786
787 let end_timestamp = env.block.time.plus_seconds(100);
788 let proposal_id = SIMPLE_VOTING
789 .new_proposal(
790 storage,
791 end_timestamp,
792 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
793 )
794 .unwrap();
795
796 SIMPLE_VOTING
798 .cast_vote(
799 deps.as_mut().storage,
800 &env.block,
801 proposal_id,
802 &Addr::unchecked("alice"),
803 Vote {
804 vote: false,
805 memo: None,
806 },
807 )
808 .unwrap();
809 let vote = SIMPLE_VOTING
810 .load_vote(
811 deps.as_ref().storage,
812 proposal_id,
813 &Addr::unchecked("alice"),
814 )
815 .unwrap()
816 .unwrap();
817 assert_eq!(
818 vote,
819 Vote {
820 vote: false,
821 memo: None
822 }
823 );
824 let proposal = SIMPLE_VOTING
825 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
826 .unwrap();
827 assert_eq!(
828 proposal,
829 ProposalInfo {
830 total_voters: 2,
831 votes_for: 0,
832 votes_against: 1,
833 status: ProposalStatus::Active,
834 config: VoteConfig {
835 threshold: Threshold::Majority {},
836 veto_duration_seconds: None
837 },
838 end_timestamp
839 }
840 );
841
842 SIMPLE_VOTING
844 .cast_vote(
845 deps.as_mut().storage,
846 &env.block,
847 proposal_id,
848 &Addr::unchecked("bob"),
849 Vote {
850 vote: false,
851 memo: Some("memo".to_owned()),
852 },
853 )
854 .unwrap();
855 let vote = SIMPLE_VOTING
856 .load_vote(deps.as_ref().storage, proposal_id, &Addr::unchecked("bob"))
857 .unwrap()
858 .unwrap();
859 assert_eq!(
860 vote,
861 Vote {
862 vote: false,
863 memo: Some("memo".to_owned())
864 }
865 );
866 let proposal = SIMPLE_VOTING
867 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
868 .unwrap();
869 assert_eq!(
870 proposal,
871 ProposalInfo {
872 total_voters: 2,
873 votes_for: 0,
874 votes_against: 2,
875 status: ProposalStatus::Active,
876 config: VoteConfig {
877 threshold: Threshold::Majority {},
878 veto_duration_seconds: None
879 },
880 end_timestamp
881 }
882 );
883
884 SIMPLE_VOTING
886 .cast_vote(
887 deps.as_mut().storage,
888 &env.block,
889 proposal_id,
890 &Addr::unchecked("alice"),
891 Vote {
892 vote: false,
893 memo: None,
894 },
895 )
896 .unwrap();
897 let proposal = SIMPLE_VOTING
899 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
900 .unwrap();
901 assert_eq!(
902 proposal,
903 ProposalInfo {
904 total_voters: 2,
905 votes_for: 0,
906 votes_against: 2,
907 status: ProposalStatus::Active,
908 config: VoteConfig {
909 threshold: Threshold::Majority {},
910 veto_duration_seconds: None
911 },
912 end_timestamp
913 }
914 );
915
916 SIMPLE_VOTING
918 .cast_vote(
919 deps.as_mut().storage,
920 &env.block,
921 proposal_id,
922 &Addr::unchecked("bob"),
923 Vote {
924 vote: true,
925 memo: None,
926 },
927 )
928 .unwrap();
929 let proposal = SIMPLE_VOTING
931 .load_proposal(deps.as_ref().storage, &env.block, proposal_id)
932 .unwrap();
933 assert_eq!(
934 proposal,
935 ProposalInfo {
936 total_voters: 2,
937 votes_for: 1,
938 votes_against: 1,
939 status: ProposalStatus::Active,
940 config: VoteConfig {
941 threshold: Threshold::Majority {},
942 veto_duration_seconds: None
943 },
944 end_timestamp
945 }
946 );
947 }
948
949 #[coverage_helper::test]
950 fn invalid_cast_votes() {
951 let mut deps = mock_dependencies();
952 let mut env = mock_env();
953 let storage = &mut deps.storage;
954 setup(
955 storage,
956 &VoteConfig {
957 threshold: Threshold::Majority {},
958 veto_duration_seconds: Some(Uint64::new(10)),
959 },
960 );
961
962 let end_timestamp = env.block.time.plus_seconds(100);
963 let proposal_id = SIMPLE_VOTING
964 .new_proposal(
965 storage,
966 end_timestamp,
967 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
968 )
969 .unwrap();
970
971 let err = SIMPLE_VOTING
973 .cast_vote(
974 deps.as_mut().storage,
975 &env.block,
976 proposal_id,
977 &Addr::unchecked("stranger"),
978 Vote {
979 vote: false,
980 memo: None,
981 },
982 )
983 .unwrap_err();
984 assert_eq!(err, VoteError::Unauthorized {});
985
986 env.block.time = end_timestamp;
988
989 let err = SIMPLE_VOTING
991 .cast_vote(
992 deps.as_mut().storage,
993 &env.block,
994 proposal_id,
995 &Addr::unchecked("alice"),
996 Vote {
997 vote: false,
998 memo: None,
999 },
1000 )
1001 .unwrap_err();
1002 assert_eq!(
1003 err,
1004 VoteError::ProposalNotActive(ProposalStatus::VetoPeriod(
1005 env.block.time.plus_seconds(10)
1006 ))
1007 );
1008
1009 env.block.time = env.block.time.plus_seconds(10);
1010
1011 let err = SIMPLE_VOTING
1013 .cast_vote(
1014 deps.as_mut().storage,
1015 &env.block,
1016 proposal_id,
1017 &Addr::unchecked("alice"),
1018 Vote {
1019 vote: false,
1020 memo: None,
1021 },
1022 )
1023 .unwrap_err();
1024 assert_eq!(
1025 err,
1026 VoteError::ProposalNotActive(ProposalStatus::WaitingForCount)
1027 );
1028
1029 SIMPLE_VOTING
1031 .count_votes(deps.as_mut().storage, &env.block, proposal_id)
1032 .unwrap();
1033 let err = SIMPLE_VOTING
1034 .cast_vote(
1035 deps.as_mut().storage,
1036 &env.block,
1037 proposal_id,
1038 &Addr::unchecked("alice"),
1039 Vote {
1040 vote: false,
1041 memo: None,
1042 },
1043 )
1044 .unwrap_err();
1045 assert_eq!(
1046 err,
1047 VoteError::ProposalNotActive(ProposalStatus::Finished(ProposalOutcome::Failed))
1048 );
1049 }
1050
1051 #[coverage_helper::test]
1052 fn count_votes() {
1053 let mut deps = mock_dependencies();
1054 let mut env = mock_env();
1055 let storage = &mut deps.storage;
1056 default_setup(storage);
1057
1058 let end_timestamp = env.block.time.plus_seconds(100);
1060 let proposal_id = SIMPLE_VOTING
1061 .new_proposal(
1062 storage,
1063 end_timestamp,
1064 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
1065 )
1066 .unwrap();
1067 env.block.time = end_timestamp.plus_seconds(10);
1068 SIMPLE_VOTING
1069 .count_votes(storage, &env.block, proposal_id)
1070 .unwrap();
1071 let proposal = SIMPLE_VOTING
1072 .load_proposal(storage, &env.block, proposal_id)
1073 .unwrap();
1074 assert_eq!(
1075 proposal,
1076 ProposalInfo {
1077 total_voters: 2,
1078 votes_for: 0,
1079 votes_against: 0,
1080 status: ProposalStatus::Finished(ProposalOutcome::Failed),
1081 config: VoteConfig {
1082 threshold: Threshold::Majority {},
1083 veto_duration_seconds: None
1084 },
1085 end_timestamp: end_timestamp.plus_seconds(10)
1086 }
1087 );
1088
1089 let end_timestamp = env.block.time.plus_seconds(100);
1091 let proposal_id = SIMPLE_VOTING
1092 .new_proposal(
1093 storage,
1094 end_timestamp,
1095 &[
1096 Addr::unchecked("alice"),
1097 Addr::unchecked("bob"),
1098 Addr::unchecked("afk"),
1099 ],
1100 )
1101 .unwrap();
1102 SIMPLE_VOTING
1103 .cast_vote(
1104 storage,
1105 &env.block,
1106 proposal_id,
1107 &Addr::unchecked("alice"),
1108 Vote {
1109 vote: true,
1110 memo: None,
1111 },
1112 )
1113 .unwrap();
1114 SIMPLE_VOTING
1115 .cast_vote(
1116 storage,
1117 &env.block,
1118 proposal_id,
1119 &Addr::unchecked("bob"),
1120 Vote {
1121 vote: true,
1122 memo: None,
1123 },
1124 )
1125 .unwrap();
1126 env.block.time = end_timestamp;
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: 3,
1137 votes_for: 2,
1138 votes_against: 0,
1139 status: ProposalStatus::Finished(ProposalOutcome::Passed),
1140 config: VoteConfig {
1141 threshold: Threshold::Majority {},
1142 veto_duration_seconds: None
1143 },
1144 end_timestamp
1145 }
1146 );
1147
1148 SIMPLE_VOTING
1150 .update_vote_config(
1151 storage,
1152 &VoteConfig {
1153 threshold: Threshold::Percentage(Decimal::percent(50)),
1154 veto_duration_seconds: None,
1155 },
1156 )
1157 .unwrap();
1158 let end_timestamp = env.block.time.plus_seconds(100);
1159 let proposal_id = SIMPLE_VOTING
1160 .new_proposal(
1161 storage,
1162 end_timestamp,
1163 &[Addr::unchecked("alice"), Addr::unchecked("bob")],
1164 )
1165 .unwrap();
1166 SIMPLE_VOTING
1167 .cast_vote(
1168 storage,
1169 &env.block,
1170 proposal_id,
1171 &Addr::unchecked("alice"),
1172 Vote {
1173 vote: true,
1174 memo: None,
1175 },
1176 )
1177 .unwrap();
1178
1179 env.block.time = end_timestamp;
1180 SIMPLE_VOTING
1181 .count_votes(storage, &env.block, proposal_id)
1182 .unwrap();
1183 let proposal = SIMPLE_VOTING
1184 .load_proposal(storage, &env.block, proposal_id)
1185 .unwrap();
1186 assert_eq!(
1187 proposal,
1188 ProposalInfo {
1189 total_voters: 2,
1190 votes_for: 1,
1191 votes_against: 0,
1192 status: ProposalStatus::Finished(ProposalOutcome::Passed),
1193 config: VoteConfig {
1194 threshold: Threshold::Percentage(Decimal::percent(50)),
1195 veto_duration_seconds: None
1196 },
1197 end_timestamp
1198 }
1199 );
1200 }
1201}