1use cosmwasm_schema::cw_serde;
2use cosmwasm_std::{Addr, BlockInfo, CosmosMsg, Decimal, Empty, Uint128};
3use cw_utils::{Expiration, Threshold};
4
5use crate::{DepositInfo, Status, Vote};
6
7const PRECISION_FACTOR: u128 = 1_000_000_000;
10
11#[cw_serde]
12pub struct Proposal {
13 pub title: String,
14 pub description: String,
15 pub start_height: u64,
16 pub expires: Expiration,
17 pub msgs: Vec<CosmosMsg<Empty>>,
18 pub status: Status,
19 pub threshold: Threshold,
21 pub total_weight: u64,
23 pub votes: Votes,
25 pub proposer: Addr,
27 pub deposit: Option<DepositInfo>,
30}
31
32impl Proposal {
33 pub fn current_status(&self, block: &BlockInfo) -> Status {
36 let mut status = self.status;
37
38 if status == Status::Open && self.is_passed(block) {
40 status = Status::Passed;
41 }
42 if status == Status::Open && (self.is_rejected(block) || self.expires.is_expired(block)) {
43 status = Status::Rejected;
44 }
45
46 status
47 }
48
49 pub fn update_status(&mut self, block: &BlockInfo) {
52 self.status = self.current_status(block);
53 }
54
55 pub fn is_passed(&self, block: &BlockInfo) -> bool {
58 match self.threshold {
59 Threshold::AbsoluteCount {
60 weight: weight_needed,
61 } => self.votes.yes >= weight_needed,
62 Threshold::AbsolutePercentage {
63 percentage: percentage_needed,
64 } => {
65 self.votes.yes
66 >= votes_needed(self.total_weight - self.votes.abstain, percentage_needed)
67 }
68 Threshold::ThresholdQuorum { threshold, quorum } => {
69 if self.votes.total() < votes_needed(self.total_weight, quorum) {
71 return false;
72 }
73 if self.expires.is_expired(block) {
74 let opinions = self.votes.total() - self.votes.abstain;
76 self.votes.yes >= votes_needed(opinions, threshold)
77 } else {
78 let possible_opinions = self.total_weight - self.votes.abstain;
80 self.votes.yes >= votes_needed(possible_opinions, threshold)
81 }
82 }
83 }
84 }
85
86 pub fn is_rejected(&self, block: &BlockInfo) -> bool {
89 match self.threshold {
90 Threshold::AbsoluteCount {
91 weight: weight_needed,
92 } => {
93 let weight = self.total_weight - weight_needed;
94 self.votes.no > weight
95 }
96 Threshold::AbsolutePercentage {
97 percentage: percentage_needed,
98 } => {
99 self.votes.no
100 > votes_needed(
101 self.total_weight - self.votes.abstain,
102 Decimal::one() - percentage_needed,
103 )
104 }
105 Threshold::ThresholdQuorum {
106 threshold,
107 quorum: _,
108 } => {
109 if self.expires.is_expired(block) {
110 let opinions = self.votes.total() - self.votes.abstain;
112 self.votes.no > votes_needed(opinions, Decimal::one() - threshold)
113 } else {
114 let possible_opinions = self.total_weight - self.votes.abstain;
116 self.votes.no > votes_needed(possible_opinions, Decimal::one() - threshold)
117 }
118 }
119 }
120 }
121}
122
123#[cw_serde]
125pub struct Votes {
126 pub yes: u64,
127 pub no: u64,
128 pub abstain: u64,
129 pub veto: u64,
130}
131
132impl Votes {
133 pub fn total(&self) -> u64 {
135 self.yes + self.no + self.abstain + self.veto
136 }
137
138 pub fn yes(init_weight: u64) -> Self {
140 Votes {
141 yes: init_weight,
142 no: 0,
143 abstain: 0,
144 veto: 0,
145 }
146 }
147
148 pub fn add_vote(&mut self, vote: Vote, weight: u64) {
149 match vote {
150 Vote::Yes => self.yes += weight,
151 Vote::Abstain => self.abstain += weight,
152 Vote::No => self.no += weight,
153 Vote::Veto => self.veto += weight,
154 }
155 }
156}
157
158fn votes_needed(weight: u64, percentage: Decimal) -> u64 {
161 let applied = Uint128::new(PRECISION_FACTOR * weight as u128).mul_floor(percentage);
162 ((applied.u128() + PRECISION_FACTOR - 1) / PRECISION_FACTOR) as u64
164}
165
166#[cw_serde]
169pub struct Ballot {
170 pub weight: u64,
171 pub vote: Vote,
172}
173
174#[cfg(test)]
175mod test {
176 use super::*;
177 use cosmwasm_std::testing::mock_env;
178
179 #[test]
180 fn count_votes() {
181 let mut votes = Votes::yes(5);
182 votes.add_vote(Vote::No, 10);
183 votes.add_vote(Vote::Veto, 20);
184 votes.add_vote(Vote::Yes, 30);
185 votes.add_vote(Vote::Abstain, 40);
186
187 assert_eq!(votes.total(), 105);
188 assert_eq!(votes.yes, 35);
189 assert_eq!(votes.no, 10);
190 assert_eq!(votes.veto, 20);
191 assert_eq!(votes.abstain, 40);
192 }
193
194 #[test]
195 fn votes_needed_rounds_properly() {
197 assert_eq!(1, votes_needed(3, Decimal::permille(333)));
199 assert_eq!(2, votes_needed(3, Decimal::permille(334)));
201 assert_eq!(11, votes_needed(30, Decimal::permille(334)));
202
203 assert_eq!(17, votes_needed(34, Decimal::percent(50)));
205 assert_eq!(12, votes_needed(48, Decimal::percent(25)));
206 }
207
208 fn setup_prop(
209 threshold: Threshold,
210 votes: Votes,
211 total_weight: u64,
212 is_expired: bool,
213 ) -> (Proposal, BlockInfo) {
214 let block = mock_env().block;
215 let expires = match is_expired {
216 true => Expiration::AtHeight(block.height - 5),
217 false => Expiration::AtHeight(block.height + 100),
218 };
219 let prop = Proposal {
220 title: "Demo".to_string(),
221 description: "Info".to_string(),
222 start_height: 100,
223 expires,
224 msgs: vec![],
225 status: Status::Open,
226 proposer: Addr::unchecked("Proposer"),
227 deposit: None,
228 threshold,
229 total_weight,
230 votes,
231 };
232
233 (prop, block)
234 }
235
236 fn check_is_passed(
237 threshold: Threshold,
238 votes: Votes,
239 total_weight: u64,
240 is_expired: bool,
241 ) -> bool {
242 let (prop, block) = setup_prop(threshold, votes, total_weight, is_expired);
243 prop.is_passed(&block)
244 }
245
246 fn check_is_rejected(
247 threshold: Threshold,
248 votes: Votes,
249 total_weight: u64,
250 is_expired: bool,
251 ) -> bool {
252 let (prop, block) = setup_prop(threshold, votes, total_weight, is_expired);
253 prop.is_rejected(&block)
254 }
255
256 #[test]
257 fn proposal_passed_absolute_count() {
258 let fixed = Threshold::AbsoluteCount { weight: 10 };
259 let mut votes = Votes::yes(7);
260 votes.add_vote(Vote::Veto, 4);
261 assert!(!check_is_passed(fixed.clone(), votes.clone(), 30, false));
263 assert!(!check_is_passed(fixed.clone(), votes.clone(), 30, true));
264 votes.add_vote(Vote::Yes, 3);
266 assert!(check_is_passed(fixed.clone(), votes.clone(), 30, false));
267 assert!(check_is_passed(fixed, votes, 30, true));
268 }
269
270 #[test]
271 fn proposal_rejected_absolute_count() {
272 let fixed = Threshold::AbsoluteCount { weight: 10 };
273 let mut votes = Votes::yes(0);
274 votes.add_vote(Vote::Veto, 4);
275 votes.add_vote(Vote::No, 7);
276 assert!(!check_is_rejected(fixed.clone(), votes.clone(), 30, false));
278 assert!(!check_is_rejected(fixed.clone(), votes.clone(), 30, true));
279 votes.add_vote(Vote::No, 14);
281 assert!(check_is_rejected(fixed.clone(), votes.clone(), 30, false));
282 assert!(check_is_rejected(fixed, votes, 30, true));
283 }
284
285 #[test]
286 fn proposal_passed_absolute_percentage() {
287 let percent = Threshold::AbsolutePercentage {
288 percentage: Decimal::percent(50),
289 };
290 let mut votes = Votes::yes(7);
291 votes.add_vote(Vote::No, 4);
292 votes.add_vote(Vote::Abstain, 2);
293 assert!(check_is_passed(percent.clone(), votes.clone(), 15, false));
296 assert!(check_is_passed(percent.clone(), votes.clone(), 15, true));
297 assert!(!check_is_passed(percent.clone(), votes.clone(), 17, false));
299
300 assert!(check_is_passed(percent.clone(), votes.clone(), 14, false));
302 assert!(check_is_passed(percent, votes, 14, true));
303 }
304
305 #[test]
306 fn proposal_rejected_absolute_percentage() {
307 let percent = Threshold::AbsolutePercentage {
308 percentage: Decimal::percent(60),
309 };
310
311 let mut votes = Votes::yes(4);
313 votes.add_vote(Vote::No, 7);
314 votes.add_vote(Vote::Abstain, 2);
315
316 assert!(check_is_rejected(percent.clone(), votes.clone(), 15, false));
319 assert!(check_is_rejected(percent.clone(), votes.clone(), 15, true));
320
321 assert!(check_is_rejected(percent.clone(), votes.clone(), 17, false));
325 assert!(check_is_rejected(percent.clone(), votes.clone(), 17, true));
326
327 assert!(!check_is_rejected(
330 percent.clone(),
331 votes.clone(),
332 20,
333 false
334 ));
335 assert!(!check_is_rejected(percent, votes.clone(), 20, true));
336 }
337
338 #[test]
339 fn proposal_passed_quorum() {
340 let quorum = Threshold::ThresholdQuorum {
341 threshold: Decimal::percent(50),
342 quorum: Decimal::percent(40),
343 };
344 let passing = Votes {
346 yes: 7,
347 no: 3,
348 abstain: 2,
349 veto: 1,
350 };
351 let passes_ignoring_abstain = Votes {
353 yes: 6,
354 no: 4,
355 abstain: 5,
356 veto: 2,
357 };
358 let failing = Votes {
360 yes: 6,
361 no: 5,
362 abstain: 2,
363 veto: 2,
364 };
365
366 assert!(check_is_passed(quorum.clone(), passing.clone(), 30, true));
369 assert!(!check_is_passed(quorum.clone(), passing.clone(), 33, true));
371 assert!(check_is_passed(
375 quorum.clone(),
376 passes_ignoring_abstain.clone(),
377 40,
378 true
379 ));
380 assert!(!check_is_passed(quorum.clone(), failing, 20, true));
382
383 assert!(!check_is_passed(quorum.clone(), passing.clone(), 30, false));
386 assert!(!check_is_passed(
387 quorum.clone(),
388 passes_ignoring_abstain.clone(),
389 40,
390 false
391 ));
392 assert!(check_is_passed(quorum.clone(), passing.clone(), 14, false));
394 assert!(check_is_passed(
396 quorum.clone(),
397 passes_ignoring_abstain,
398 17,
399 false
400 ));
401 assert!(check_is_passed(quorum, passing, 16, false));
403 }
404
405 #[test]
406 fn proposal_rejected_quorum() {
407 let quorum = Threshold::ThresholdQuorum {
408 threshold: Decimal::percent(60),
409 quorum: Decimal::percent(40),
410 };
411 let rejecting = Votes {
413 yes: 3,
414 no: 7,
415 abstain: 2,
416 veto: 1,
417 };
418 let rejected_ignoring_abstain = Votes {
420 yes: 4,
421 no: 6,
422 abstain: 5,
423 veto: 2,
424 };
425 let failing = Votes {
427 yes: 5,
428 no: 5,
429 abstain: 2,
430 veto: 3,
431 };
432
433 assert!(check_is_rejected(
439 quorum.clone(),
440 rejecting.clone(),
441 30,
442 true
443 ));
444
445 assert!(!check_is_rejected(
447 quorum.clone(),
448 rejecting.clone(),
449 50,
450 false
451 ));
452 assert!(check_is_rejected(
454 quorum.clone(),
455 rejecting.clone(),
456 50,
457 true
458 ));
459
460 let quorum_edgecase = Threshold::ThresholdQuorum {
463 threshold: Decimal::percent(67),
464 quorum: Decimal::percent(40),
465 };
466 assert!(check_is_rejected(
467 quorum_edgecase,
468 Votes {
469 yes: 15,
470 no: 35,
471 abstain: 0,
472 veto: 10
473 },
474 100,
475 true
476 ));
477
478 assert!(check_is_rejected(
484 quorum.clone(),
485 rejected_ignoring_abstain.clone(),
486 40,
487 true
488 ));
489
490 assert!(!check_is_rejected(quorum.clone(), failing, 20, true));
494
495 assert!(check_is_rejected(
501 quorum.clone(),
502 rejecting.clone(),
503 14,
504 false
505 ));
506 assert!(check_is_rejected(
513 quorum.clone(),
514 rejected_ignoring_abstain,
515 17,
516 false
517 ));
518
519 assert!(check_is_rejected(quorum, rejecting, 16, false));
521 }
522
523 #[test]
524 fn quorum_edge_cases() {
525 let quorum = Threshold::ThresholdQuorum {
527 threshold: Decimal::percent(60),
528 quorum: Decimal::percent(80),
529 };
530
531 let missing_voters = Votes {
534 yes: 9,
535 no: 1,
536 abstain: 0,
537 veto: 0,
538 };
539 assert!(!check_is_passed(
540 quorum.clone(),
541 missing_voters.clone(),
542 15,
543 false
544 ));
545 assert!(!check_is_passed(quorum.clone(), missing_voters, 15, true));
546
547 let wait_til_expired = Votes {
549 yes: 8,
550 no: 1,
551 abstain: 0,
552 veto: 3,
553 };
554 assert!(!check_is_passed(
555 quorum.clone(),
556 wait_til_expired.clone(),
557 15,
558 false
559 ));
560 assert!(check_is_passed(quorum.clone(), wait_til_expired, 15, true));
561
562 let passes_early = Votes {
564 yes: 9,
565 no: 3,
566 abstain: 0,
567 veto: 0,
568 };
569 assert!(check_is_passed(
570 quorum.clone(),
571 passes_early.clone(),
572 15,
573 false
574 ));
575 assert!(check_is_passed(quorum, passes_early, 15, true));
576 }
577}