use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use cosmwasm_std::{Addr, BlockInfo, Decimal, StdResult, Storage, Uint128};
use cw_storage_plus::{Item, Map};
use tg3::{Status, Vote};
use tg4::Tg4Contract;
use tg_utils::Expiration;
use crate::ContractError;
const PRECISION_FACTOR: u128 = 1_000_000_000;
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Config {
pub rules: VotingRules,
pub group_contract: Tg4Contract,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Proposal<P> {
pub title: String,
pub description: String,
pub start_height: u64,
pub created_by: String,
pub expires: Expiration,
pub proposal: P,
pub status: Status,
pub rules: VotingRules,
pub total_points: u64,
pub votes: Votes,
}
impl<P> From<Proposal<P>> for ProposalInfo {
fn from(p: Proposal<P>) -> Self {
Self {
title: p.title,
description: p.description,
}
}
}
impl<P> Proposal<P> {
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 {
let VotingRules {
quorum,
threshold,
allow_end_early,
..
} = self.rules;
if self.votes.total() < votes_needed(self.total_points, 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 if allow_end_early {
let possible_opinions = self.total_points - self.votes.abstain;
self.votes.yes >= votes_needed(possible_opinions, threshold)
} else {
false
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ProposalResponse<P> {
pub id: u64,
pub title: String,
pub description: String,
pub created_by: String,
pub proposal: P,
pub status: Status,
pub expires: Expiration,
pub rules: VotingRules,
pub total_points: u64,
pub votes: Votes,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ProposalListResponse<P> {
pub proposals: Vec<ProposalResponse<P>>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct TextProposalListResponse {
pub proposals: Vec<ProposalInfo>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq, Debug, JsonSchema)]
pub struct VotingRules {
pub voting_period: u32,
pub quorum: Decimal,
pub threshold: Decimal,
pub allow_end_early: bool,
}
impl VotingRules {
pub fn validate(&self) -> Result<(), ContractError> {
let zero = Decimal::percent(0);
let hundred = Decimal::percent(100);
if self.quorum == zero || self.quorum > hundred {
return Err(ContractError::InvalidQuorum(self.quorum));
}
if self.threshold < Decimal::percent(50) || self.threshold > hundred {
return Err(ContractError::InvalidThreshold(self.threshold));
}
if self.voting_period == 0 || self.voting_period > 365 {
return Err(ContractError::InvalidVotingPeriod(self.voting_period));
}
Ok(())
}
pub fn voting_period_secs(&self) -> u64 {
self.voting_period as u64 * 86_400
}
}
pub struct RulesBuilder {
voting_period: u32,
quorum: Decimal,
threshold: Decimal,
allow_end_early: bool,
}
impl RulesBuilder {
pub fn new() -> Self {
Self {
voting_period: 14,
quorum: Decimal::percent(20),
threshold: Decimal::percent(50),
allow_end_early: true,
}
}
pub fn with_threshold(mut self, threshold: impl Into<Decimal>) -> Self {
self.threshold = threshold.into();
self
}
pub fn with_quorum(mut self, quorum: impl Into<Decimal>) -> Self {
self.quorum = quorum.into();
self
}
pub fn with_allow_early(mut self, allow_end_early: bool) -> Self {
self.allow_end_early = allow_end_early;
self
}
pub fn build(&self) -> VotingRules {
VotingRules {
voting_period: self.voting_period,
quorum: self.quorum,
threshold: self.threshold,
allow_end_early: self.allow_end_early,
}
}
}
impl Default for RulesBuilder {
fn default() -> Self {
Self::new()
}
}
#[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 yes(init_points: u64) -> Self {
Votes {
yes: init_points,
no: 0,
abstain: 0,
veto: 0,
}
}
pub fn add_vote(&mut self, vote: Vote, points: u64) {
match vote {
Vote::Yes => self.yes += points,
Vote::Abstain => self.abstain += points,
Vote::No => self.no += points,
Vote::Veto => self.veto += points,
}
}
}
fn votes_needed(points: u64, percentage: Decimal) -> u64 {
let applied = percentage * Uint128::new(PRECISION_FACTOR * points as u128);
((applied.u128() + PRECISION_FACTOR - 1) / PRECISION_FACTOR) as u64
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct Ballot {
pub points: u64,
pub vote: Vote,
}
pub const CONFIG: Item<Config> = Item::new("voting_config");
pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
pub const BALLOTS: Map<(u64, &Addr), Ballot> = Map::new("votes");
pub const BALLOTS_BY_VOTER: Map<(&Addr, u64), Ballot> = Map::new("votes_by_voter");
pub fn proposals<'m, P>() -> Map<'m, u64, Proposal<P>> {
Map::new("proposals")
}
#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)]
pub struct ProposalInfo {
pub title: String,
pub description: String,
}
pub const TEXT_PROPOSALS: Map<u64, ProposalInfo> = Map::new("text_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)
}