abstract_std/objects/
voting.rs

1//! # Simple voting
2//! Simple voting is a state object to enable voting mechanism on a contract
3//!
4//! ## Setting up
5//! * Create SimpleVoting object in similar way to the cw-storage-plus objects using [`SimpleVoting::new`] method
6//! * Inside instantiate contract method use [`SimpleVoting::instantiate`] method
7//! * Add [`VoteError`] type to your application errors
8//!
9//! ## Creating a new proposal
10//! To create a new proposal use [`SimpleVoting::new_proposal`] method, it will return ProposalId
11//!
12//! ## Whitelisting voters
13//! Initial whitelist passed during [`SimpleVoting::new_proposal`] method and currently has no way to edit this
14//!
15//! ## Voting
16//! To cast a vote use [`SimpleVoting::cast_vote`] method
17//!
18//! ## Count voting
19//! To count votes use [`SimpleVoting::count_votes`] method during [`ProposalStatus::WaitingForCount`]
20//!
21//! ## Veto
22//! In case your [`VoteConfig`] has veto duration set-up, after proposal.end_timestamp veto period will start
23//! * During veto period [`SimpleVoting::veto_proposal`] method could be used to Veto proposal
24//!
25//! ## Cancel proposal
26//! During active voting:
27//! * [`SimpleVoting::cancel_proposal`] method could be used to cancel proposal
28//!
29//! ## Queries
30//! * Single-item queries methods allowed by `load_` prefix
31//! * List of items queries allowed by `query_` prefix
32//!
33//! ## Details
34//! All methods that modify proposal will return [`ProposalInfo`] to allow logging or checking current status of proposal.
35//!
36//! Each proposal goes through the following stages:
37//! 1. Active: proposal is active and can be voted on. It can also be canceled during this period.
38//! 3. VetoPeriod (optional): voting is counted and veto period is active.
39//! 2. WaitingForCount: voting period is finished and awaiting counting.
40//! 4. Finished: proposal is finished and count is done. The proposal then has one of the following end states:
41//!     * Passed: proposal passed
42//!     * Failed: proposal failed
43//!     * Canceled: proposal was canceled
44//!     * Vetoed: proposal was vetoed
45
46use 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
86/// Simple voting helper
87pub 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    /// SimpleVoting setup during instantiation
110    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    /// Create new proposal
127    /// initial_voters is a list of whitelisted to vote
128    pub fn new_proposal(
129        &self,
130        store: &mut dyn Storage,
131        end: Timestamp,
132        initial_voters: &[Addr],
133    ) -> VoteResult<ProposalId> {
134        // Check if addrs unique
135        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    /// Assign vote for the voter
157    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                // We allow re-voting
173                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    // Note: this method doesn't check a sender
187    // Therefore caller of this method should check if he is allowed to cancel vote
188    /// Cancel proposal
189    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    /// Count votes and finish or move to the veto period(if configured) for this proposal
205    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        // Calculate votes
221        let threshold = match vote_config.threshold {
222            // 50% + 1 voter
223            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        // Update vote status
236        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    /// Called by veto admin
244    /// Finish or Veto this proposal
245    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    /// Load vote by address
267    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    /// Load proposal by id with updated status if required
279    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            // Check if veto or count period and update if so
298            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    /// Load current vote config
310    pub fn load_config(&self, store: &dyn Storage) -> StdResult<VoteConfig> {
311        self.vote_config.load(store)
312    }
313
314    /// List of votes by proposal id
315    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/// Vote struct
354#[cosmwasm_schema::cw_serde]
355pub struct Vote {
356    /// true: Vote for
357    /// false: Vote against
358    pub vote: bool,
359    /// memo for the vote
360    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    /// Config it was created with
370    /// For cases config got changed during voting
371    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            // unchanged vote
394            (Some(Vote { vote: true, .. }), true) | (Some(Vote { vote: false, .. }), false) => {}
395            // vote for became vote against
396            (Some(Vote { vote: true, .. }), false) => {
397                self.votes_against += 1;
398                self.votes_for -= 1;
399            }
400            // vote against became vote for
401            (Some(Vote { vote: false, .. }), true) => {
402                self.votes_for += 1;
403                self.votes_against -= 1;
404            }
405            // new vote for
406            (None, true) => {
407                self.votes_for += 1;
408            }
409            // new vote against
410            (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    /// Veto duration after the first vote
474    /// None disables veto
475    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    /// Asserts that the 0.0 < percent <= 1.0
486    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        // Normal proposal
551        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        // Not active
565        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        // Create one proposal
583        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        // Create another proposal (already expired)
611        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        // Create one proposal
668        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                // Finish time here
695                end_timestamp: env.block.time
696            }
697        );
698
699        // Can't cancel during non-active
700        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    // Check it updates status when required
710    #[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        // Should auto-update to the veto
733        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        // Should update to the WaitingForCount
743        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        // Should update to the Finished
750        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        // Should auto-update to the waiting if not configured veto period
773        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        // Alice vote
797        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        // Bob votes
843        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        // re-cast votes(to the same vote)
885        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        // unchanged
898        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        // re-cast votes(to the opposite vote)
917        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        // unchanged
930        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        // Stranger vote
972        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        // Vote during veto
987        env.block.time = end_timestamp;
988
989        // Vote during veto
990        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        // Too late vote
1012        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        // Post-finish votes
1030        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        // Failed proposal
1059        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        // Succeeded proposal 2/3 majority
1090        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        // Succeeded proposal 1/2 50% Decimal
1149        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}