abstract_core/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<'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    /// 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) => 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        // Update vote status
234        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    /// Called by veto admin
242    /// Finish or Veto this proposal
243    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    // TODO: It's currently not used, and most likely not desirable to edit voters during active voting
265    // In case it will get some use: keep in mind that it's untested
266
267    // /// Add new addresses that's allowed to vote
268    // pub fn add_voters(
269    //     &self,
270    //     store: &mut dyn Storage,
271    //     proposal_id: ProposalId,
272    //     block: &BlockInfo,
273    //     new_voters: &[Addr],
274    // ) -> VoteResult<ProposalInfo> {
275    //     // Need to check it's existing proposal
276    //     let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
277    //     proposal_info.assert_active_proposal()?;
278
279    //     for voter in new_voters {
280    //         // Don't override already existing vote
281    //         self.proposals
282    //             .update(store, (proposal_id, voter), |v| match v {
283    //                 Some(_) => Err(VoteError::DuplicateAddrs {}),
284    //                 None => {
285    //                     proposal_info.total_voters += 1;
286    //                     Ok(None)
287    //                 }
288    //             })?;
289    //     }
290    //     self.proposals_info
291    //         .save(store, proposal_id, &proposal_info)?;
292
293    //     Ok(proposal_info)
294    // }
295
296    // /// Remove addresses that's allowed to vote
297    // /// Will re-count votes
298    // pub fn remove_voters(
299    //     &self,
300    //     store: &mut dyn Storage,
301    //     proposal_id: ProposalId,
302    //     block: &BlockInfo,
303    //     removed_voters: &[Addr],
304    // ) -> VoteResult<ProposalInfo> {
305    //     let mut proposal_info = self.load_proposal(store, block, proposal_id)?;
306    //     proposal_info.assert_active_proposal()?;
307
308    //     for voter in removed_voters {
309    //         if let Some(vote) = self.proposals.may_load(store, (proposal_id, voter))? {
310    //             if let Some(previous_vote) = vote {
311    //                 match previous_vote.vote {
312    //                     true => proposal_info.votes_for -= 1,
313    //                     false => proposal_info.votes_against -= 1,
314    //                 }
315    //             }
316    //             proposal_info.total_voters -= 1;
317    //             self.proposals.remove(store, (proposal_id, voter));
318    //         }
319    //     }
320    //     self.proposals_info
321    //         .save(store, proposal_id, &proposal_info)?;
322    //     Ok(proposal_info)
323    // }
324
325    /// Load vote by address
326    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    /// Load proposal by id with updated status if required
338    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            // Check if veto or count period and update if so
357            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    /// Load current vote config
369    pub fn load_config(&self, store: &dyn Storage) -> StdResult<VoteConfig> {
370        self.vote_config.load(store)
371    }
372
373    /// List of votes by proposal id
374    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/// Vote struct
413#[cosmwasm_schema::cw_serde]
414pub struct Vote {
415    /// true: Vote for
416    /// false: Vote against
417    pub vote: bool,
418    /// memo for the vote
419    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    /// Config it was created with
429    /// For cases config got changed during voting
430    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            // unchanged vote
453            (Some(Vote { vote: true, .. }), true) | (Some(Vote { vote: false, .. }), false) => {}
454            // vote for became vote against
455            (Some(Vote { vote: true, .. }), false) => {
456                self.votes_against += 1;
457                self.votes_for -= 1;
458            }
459            // vote against became vote for
460            (Some(Vote { vote: false, .. }), true) => {
461                self.votes_for += 1;
462                self.votes_against -= 1;
463            }
464            // new vote for
465            (None, true) => {
466                self.votes_for += 1;
467            }
468            // new vote against
469            (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    /// Veto duration after the first vote
533    /// None disables veto
534    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    /// Asserts that the 0.0 < percent <= 1.0
545    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        // Normal proposal
610        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        // Not active
624        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        // Create one proposal
642        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        // Create another proposal (already expired)
670        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        // Create one proposal
727        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                // Finish time here
754                end_timestamp: env.block.time
755            }
756        );
757
758        // Can't cancel during non-active
759        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    // Check it updates status when required
769    #[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        // Should auto-update to the veto
792        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        // Should update to the WaitingForCount
802        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        // Should update to the Finished
809        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        // Should auto-update to the waiting if not configured veto period
832        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        // Alice vote
856        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        // Bob votes
902        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        // re-cast votes(to the same vote)
944        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        // unchanged
957        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        // re-cast votes(to the opposite vote)
976        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        // unchanged
989        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        // Stranger vote
1031        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        // Vote during veto
1046        env.block.time = end_timestamp;
1047
1048        // Vote during veto
1049        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        // Too late vote
1071        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        // Post-finish votes
1089        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        // Failed proposal
1118        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        // Succeeded proposal 2/3 majority
1149        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        // Succeeded proposal 1/2 50% Decimal
1208        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}