dao_voting/
multiple_choice.rs

1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{CosmosMsg, Empty, StdError, StdResult, Uint128};
3
4use crate::threshold::{validate_quorum, PercentageThreshold, ThresholdError};
5
6/// Maximum number of choices for multiple choice votes. Chosen
7/// in order to impose a bound on state / queries.
8pub const MAX_NUM_CHOICES: u32 = 20;
9const NONE_OPTION_DESCRIPTION: &str = "None of the above";
10
11/// Determines how many choices may be selected.
12#[cw_serde]
13pub enum VotingStrategy {
14    SingleChoice { quorum: PercentageThreshold },
15}
16
17impl VotingStrategy {
18    pub fn validate(&self) -> Result<(), ThresholdError> {
19        match self {
20            VotingStrategy::SingleChoice { quorum } => validate_quorum(quorum),
21        }
22    }
23
24    pub fn get_quorum(&self) -> PercentageThreshold {
25        match self {
26            VotingStrategy::SingleChoice { quorum } => *quorum,
27        }
28    }
29}
30
31/// A multiple choice vote, picking the desired option
32#[cw_serde]
33#[derive(Copy)]
34pub struct MultipleChoiceVote {
35    // A vote indicates which option the user has selected.
36    pub option_id: u32,
37}
38
39impl std::fmt::Display for MultipleChoiceVote {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        write!(f, "{}", self.option_id)
42    }
43}
44
45// Holds the vote weights for each option
46#[cw_serde]
47pub struct MultipleChoiceVotes {
48    // Vote counts is a vector of integers indicating the vote weight for each option
49    // (the index corresponds to the option).
50    pub vote_weights: Vec<Uint128>,
51}
52
53impl MultipleChoiceVotes {
54    /// Sum of all vote weights
55    pub fn total(&self) -> Uint128 {
56        self.vote_weights.iter().sum()
57    }
58
59    // Add a vote to the tally
60    pub fn add_vote(&mut self, vote: MultipleChoiceVote, weight: Uint128) -> StdResult<()> {
61        self.vote_weights[vote.option_id as usize] = self.vote_weights[vote.option_id as usize]
62            .checked_add(weight)
63            .map_err(StdError::overflow)?;
64        Ok(())
65    }
66
67    // Remove a vote from the tally
68    pub fn remove_vote(&mut self, vote: MultipleChoiceVote, weight: Uint128) -> StdResult<()> {
69        self.vote_weights[vote.option_id as usize] = self.vote_weights[vote.option_id as usize]
70            .checked_sub(weight)
71            .map_err(StdError::overflow)?;
72        Ok(())
73    }
74
75    // Default tally of zero for all multiple choice options
76    pub fn zero(num_choices: usize) -> Self {
77        Self {
78            vote_weights: vec![Uint128::zero(); num_choices],
79        }
80    }
81}
82
83/// Represents the type of Multiple choice option. "None of the above" has a special
84/// type for example.
85#[cw_serde]
86pub enum MultipleChoiceOptionType {
87    /// Choice that represents selecting none of the options; still counts toward quorum
88    /// and allows proposals with all bad options to be voted against.
89    None,
90    Standard,
91}
92
93/// Represents unchecked multiple choice options
94#[cw_serde]
95pub struct MultipleChoiceOptions {
96    pub options: Vec<MultipleChoiceOption>,
97}
98
99/// Unchecked multiple choice option
100#[cw_serde]
101pub struct MultipleChoiceOption {
102    pub title: String,
103    pub description: String,
104    pub msgs: Vec<CosmosMsg<Empty>>,
105}
106
107/// Multiple choice options that have been verified for correctness, and have all fields
108/// necessary for voting.
109#[cw_serde]
110pub struct CheckedMultipleChoiceOptions {
111    pub options: Vec<CheckedMultipleChoiceOption>,
112}
113
114/// A verified option that has all fields needed for voting.
115#[cw_serde]
116pub struct CheckedMultipleChoiceOption {
117    // This is the index of the option in both the vote_weights and proposal.choices vectors.
118    // Workaround due to not being able to use HashMaps in Cosmwasm.
119    pub index: u32,
120    pub option_type: MultipleChoiceOptionType,
121    pub title: String,
122    pub description: String,
123    pub msgs: Vec<CosmosMsg<Empty>>,
124    pub vote_count: Uint128,
125}
126
127impl MultipleChoiceOptions {
128    pub fn into_checked(self) -> StdResult<CheckedMultipleChoiceOptions> {
129        if self.options.len() < 2 || self.options.len() > MAX_NUM_CHOICES as usize {
130            return Err(StdError::GenericErr {
131                msg: "Wrong number of choices".to_string(),
132            });
133        }
134
135        let mut checked_options: Vec<CheckedMultipleChoiceOption> =
136            Vec::with_capacity(self.options.len() + 1);
137
138        // Iterate through choices and save the index and option type for each
139        self.options
140            .into_iter()
141            .enumerate()
142            .for_each(|(idx, choice)| {
143                let checked_option = CheckedMultipleChoiceOption {
144                    index: idx as u32,
145                    option_type: MultipleChoiceOptionType::Standard,
146                    description: choice.description,
147                    msgs: choice.msgs,
148                    vote_count: Uint128::zero(),
149                    title: choice.title,
150                };
151                checked_options.push(checked_option)
152            });
153
154        // Add a "None of the above" option, required for every multiple choice proposal.
155        let none_option = CheckedMultipleChoiceOption {
156            index: (checked_options.capacity() - 1) as u32,
157            option_type: MultipleChoiceOptionType::None,
158            description: NONE_OPTION_DESCRIPTION.to_string(),
159            msgs: vec![],
160            vote_count: Uint128::zero(),
161            title: NONE_OPTION_DESCRIPTION.to_string(),
162        };
163
164        checked_options.push(none_option);
165
166        let options = CheckedMultipleChoiceOptions {
167            options: checked_options,
168        };
169        Ok(options)
170    }
171}
172
173#[cw_serde]
174pub struct MultipleChoiceAutoVote {
175    /// The proposer's position on the proposal.
176    pub vote: MultipleChoiceVote,
177    /// An optional rationale for why this vote was cast. This can
178    /// be updated, set, or removed later by the address casting
179    /// the vote.
180    pub rationale: Option<String>,
181}
182
183#[cfg(test)]
184mod test {
185    use std::vec;
186
187    use super::*;
188
189    #[test]
190    fn test_display_multiple_choice_vote() {
191        let vote = MultipleChoiceVote { option_id: 0 };
192        assert_eq!("0", vote.to_string())
193    }
194
195    #[test]
196    fn test_multiple_choice_votes() {
197        let mut votes = MultipleChoiceVotes {
198            vote_weights: vec![Uint128::new(10), Uint128::new(100)],
199        };
200        let total = votes.total();
201        assert_eq!(total, Uint128::new(110));
202
203        votes
204            .add_vote(MultipleChoiceVote { option_id: 0 }, Uint128::new(10))
205            .unwrap();
206        let total = votes.total();
207        assert_eq!(total, Uint128::new(120));
208
209        votes
210            .remove_vote(MultipleChoiceVote { option_id: 0 }, Uint128::new(20))
211            .unwrap();
212        votes
213            .remove_vote(MultipleChoiceVote { option_id: 1 }, Uint128::new(100))
214            .unwrap();
215
216        assert_eq!(votes, MultipleChoiceVotes::zero(2))
217    }
218
219    #[test]
220    fn test_into_checked() {
221        let options = vec![
222            super::MultipleChoiceOption {
223                description: "multiple choice option 1".to_string(),
224                msgs: vec![],
225                title: "title".to_string(),
226            },
227            super::MultipleChoiceOption {
228                description: "multiple choice option 2".to_string(),
229                msgs: vec![],
230                title: "title".to_string(),
231            },
232        ];
233
234        let mc_options = super::MultipleChoiceOptions { options };
235
236        let checked_mc_options = mc_options.into_checked().unwrap();
237        assert_eq!(checked_mc_options.options.len(), 3);
238        assert_eq!(
239            checked_mc_options.options[0].option_type,
240            super::MultipleChoiceOptionType::Standard
241        );
242        assert_eq!(
243            checked_mc_options.options[0].description,
244            "multiple choice option 1",
245        );
246        assert_eq!(
247            checked_mc_options.options[1].option_type,
248            super::MultipleChoiceOptionType::Standard
249        );
250        assert_eq!(
251            checked_mc_options.options[1].description,
252            "multiple choice option 2",
253        );
254        assert_eq!(
255            checked_mc_options.options[2].option_type,
256            super::MultipleChoiceOptionType::None
257        );
258        assert_eq!(
259            checked_mc_options.options[2].description,
260            super::NONE_OPTION_DESCRIPTION,
261        );
262    }
263
264    #[should_panic(expected = "Wrong number of choices")]
265    #[test]
266    fn test_into_checked_wrong_num_choices() {
267        let options = vec![super::MultipleChoiceOption {
268            description: "multiple choice option 1".to_string(),
269            msgs: vec![],
270            title: "title".to_string(),
271        }];
272
273        let mc_options = super::MultipleChoiceOptions { options };
274        mc_options.into_checked().unwrap();
275    }
276}