use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use cosmwasm_std::{BlockInfo, CosmosMsg, Decimal, Empty, StdError, StdResult, Storage, Uint128};
use cw0::{Duration, Expiration};
use cw3::{Status, Vote};
use cw4::Cw4Contract;
use cw_storage_plus::{Item, Map, U64Key};
use crate::msg::Threshold;
const PRECISION_FACTOR: u128 = 1_000_000_000;
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Config {
pub threshold: Threshold,
pub max_voting_period: Duration,
pub group_addr: Cw4Contract,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Proposal {
pub title: String,
pub description: String,
pub start_height: u64,
pub expires: Expiration,
pub msgs: Vec<CosmosMsg<Empty>>,
pub status: Status,
pub threshold: Threshold,
pub total_weight: u64,
pub votes: Votes,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Votes {
pub yes: u64,
pub no: u64,
pub abstain: u64,
pub veto: u64,
}
impl Votes {
pub fn total(&self) -> u64 {
self.yes + self.no + self.abstain + self.veto
}
pub fn new(init_weight: u64) -> Self {
Votes {
yes: init_weight,
no: 0,
abstain: 0,
veto: 0,
}
}
pub fn add_vote(&mut self, vote: Vote, weight: u64) {
match vote {
Vote::Yes => self.yes += weight,
Vote::Abstain => self.abstain += weight,
Vote::No => self.no += weight,
Vote::Veto => self.veto += weight,
}
}
}
impl Proposal {
pub fn current_status(&self, block: &BlockInfo) -> Status {
let mut status = self.status;
if status == Status::Open && self.is_passed(block) {
status = Status::Passed;
}
if status == Status::Open && self.expires.is_expired(block) {
status = Status::Rejected;
}
status
}
pub fn update_status(&mut self, block: &BlockInfo) {
self.status = self.current_status(block);
}
pub fn is_passed(&self, block: &BlockInfo) -> bool {
match self.threshold {
Threshold::AbsoluteCount {
weight: weight_needed,
} => self.votes.yes >= weight_needed,
Threshold::AbsolutePercentage {
percentage: percentage_needed,
} => {
self.votes.yes
>= votes_needed(self.total_weight - self.votes.abstain, percentage_needed)
}
Threshold::ThresholdQuorum { threshold, quorum } => {
if self.votes.total() < votes_needed(self.total_weight, quorum) {
return false;
}
if self.expires.is_expired(block) {
let opinions = self.votes.total() - self.votes.abstain;
self.votes.yes >= votes_needed(opinions, threshold)
} else {
let possible_opinions = self.total_weight - self.votes.abstain;
self.votes.yes >= votes_needed(possible_opinions, threshold)
}
}
}
}
}
fn votes_needed(weight: u64, percentage: Decimal) -> u64 {
let applied = percentage * Uint128(PRECISION_FACTOR * weight as u128);
((applied.u128() + PRECISION_FACTOR - 1) / PRECISION_FACTOR) as u64
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Ballot {
pub weight: u64,
pub vote: Vote,
}
pub const CONFIG: Item<Config> = Item::new("config");
pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
pub const BALLOTS: Map<(U64Key, &[u8]), Ballot> = Map::new("votes");
pub const PROPOSALS: Map<U64Key, Proposal> = Map::new("proposals");
pub fn next_id(store: &mut dyn Storage) -> StdResult<u64> {
let id: u64 = PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1;
PROPOSAL_COUNT.save(store, &id)?;
Ok(id)
}
pub fn parse_id(data: &[u8]) -> StdResult<u64> {
match data[0..8].try_into() {
Ok(bytes) => Ok(u64::from_be_bytes(bytes)),
Err(_) => Err(StdError::generic_err(
"Corrupted data found. 8 byte expected.",
)),
}
}
#[cfg(test)]
mod test {
use super::*;
use cosmwasm_std::testing::mock_env;
#[test]
fn count_votes() {
let mut votes = Votes::new(5);
votes.add_vote(Vote::No, 10);
votes.add_vote(Vote::Veto, 20);
votes.add_vote(Vote::Yes, 30);
votes.add_vote(Vote::Abstain, 40);
assert_eq!(votes.total(), 105);
assert_eq!(votes.yes, 35);
assert_eq!(votes.no, 10);
assert_eq!(votes.veto, 20);
assert_eq!(votes.abstain, 40);
}
#[test]
fn votes_needed_rounds_properly() {
assert_eq!(1, votes_needed(3, Decimal::permille(333)));
assert_eq!(2, votes_needed(3, Decimal::permille(334)));
assert_eq!(11, votes_needed(30, Decimal::permille(334)));
assert_eq!(17, votes_needed(34, Decimal::percent(50)));
assert_eq!(12, votes_needed(48, Decimal::percent(25)));
}
fn check_is_passed(
threshold: Threshold,
votes: Votes,
total_weight: u64,
is_expired: bool,
) -> bool {
let block = mock_env().block;
let expires = match is_expired {
true => Expiration::AtHeight(block.height - 5),
false => Expiration::AtHeight(block.height + 100),
};
let prop = Proposal {
title: "Demo".to_string(),
description: "Info".to_string(),
start_height: 100,
expires,
msgs: vec![],
status: Status::Open,
threshold,
total_weight,
votes,
};
prop.is_passed(&block)
}
#[test]
fn proposal_passed_absolute_count() {
let fixed = Threshold::AbsoluteCount { weight: 10 };
let mut votes = Votes::new(7);
votes.add_vote(Vote::Veto, 4);
assert_eq!(
false,
check_is_passed(fixed.clone(), votes.clone(), 30, false)
);
assert_eq!(
false,
check_is_passed(fixed.clone(), votes.clone(), 30, true)
);
votes.add_vote(Vote::Yes, 3);
assert_eq!(
true,
check_is_passed(fixed.clone(), votes.clone(), 30, false)
);
assert_eq!(
true,
check_is_passed(fixed.clone(), votes.clone(), 30, true)
);
}
#[test]
fn proposal_passed_absolute_percentage() {
let percent = Threshold::AbsolutePercentage {
percentage: Decimal::percent(50),
};
let mut votes = Votes::new(7);
votes.add_vote(Vote::No, 4);
votes.add_vote(Vote::Abstain, 2);
assert_eq!(
true,
check_is_passed(percent.clone(), votes.clone(), 15, false)
);
assert_eq!(
true,
check_is_passed(percent.clone(), votes.clone(), 15, true)
);
assert_eq!(
false,
check_is_passed(percent.clone(), votes.clone(), 17, false)
);
assert_eq!(
true,
check_is_passed(percent.clone(), votes.clone(), 14, false)
);
assert_eq!(
true,
check_is_passed(percent.clone(), votes.clone(), 14, true)
);
}
#[test]
fn proposal_passed_quorum() {
let quorum = Threshold::ThresholdQuorum {
threshold: Decimal::percent(50),
quorum: Decimal::percent(40),
};
let passing = Votes {
yes: 7,
no: 3,
abstain: 2,
veto: 1,
};
let passes_ignoring_abstain = Votes {
yes: 6,
no: 4,
abstain: 5,
veto: 2,
};
let failing = Votes {
yes: 6,
no: 5,
abstain: 2,
veto: 2,
};
assert_eq!(
true,
check_is_passed(quorum.clone(), passing.clone(), 30, true)
);
assert_eq!(
false,
check_is_passed(quorum.clone(), passing.clone(), 33, true)
);
assert_eq!(
true,
check_is_passed(quorum.clone(), passes_ignoring_abstain.clone(), 40, true)
);
assert_eq!(
false,
check_is_passed(quorum.clone(), failing.clone(), 20, true)
);
assert_eq!(
false,
check_is_passed(quorum.clone(), passing.clone(), 30, false)
);
assert_eq!(
false,
check_is_passed(quorum.clone(), passes_ignoring_abstain.clone(), 40, false)
);
assert_eq!(
true,
check_is_passed(quorum.clone(), passing.clone(), 14, false)
);
assert_eq!(
true,
check_is_passed(quorum.clone(), passes_ignoring_abstain.clone(), 17, false)
);
assert_eq!(
true,
check_is_passed(quorum.clone(), passing.clone(), 16, false)
);
}
#[test]
fn quorum_edge_cases() {
let quorum = Threshold::ThresholdQuorum {
threshold: Decimal::percent(60),
quorum: Decimal::percent(80),
};
let missing_voters = Votes {
yes: 9,
no: 1,
abstain: 0,
veto: 0,
};
assert_eq!(
false,
check_is_passed(quorum.clone(), missing_voters.clone(), 15, false)
);
assert_eq!(
false,
check_is_passed(quorum.clone(), missing_voters.clone(), 15, true)
);
let wait_til_expired = Votes {
yes: 8,
no: 1,
abstain: 0,
veto: 3,
};
assert_eq!(
false,
check_is_passed(quorum.clone(), wait_til_expired.clone(), 15, false)
);
assert_eq!(
true,
check_is_passed(quorum.clone(), wait_til_expired.clone(), 15, true)
);
let passes_early = Votes {
yes: 9,
no: 3,
abstain: 0,
veto: 0,
};
assert_eq!(
true,
check_is_passed(quorum.clone(), passes_early.clone(), 15, false)
);
assert_eq!(
true,
check_is_passed(quorum.clone(), passes_early.clone(), 15, true)
);
}
}