1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use cosmwasm_std::{BlockInfo, Decimal, StdResult, Storage, Uint128};
5use cw_storage_plus::{Item, Map};
6use tg3::{Status, Vote};
7use tg4::Tg4Contract;
8use tg_utils::Expiration;
9
10use crate::ContractError;
11
12const PRECISION_FACTOR: u128 = 1_000_000_000;
15
16#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
18pub struct Config {
19 pub rules: VotingRules,
20 pub group_contract: Tg4Contract,
22}
23
24#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
25pub struct Proposal<P> {
26 pub title: String,
27 pub description: String,
28 pub start_height: u64,
29 pub created_by: String,
30 pub expires: Expiration,
31 pub proposal: P,
32 pub status: Status,
33 pub rules: VotingRules,
35 pub total_points: u64,
37 pub votes: Votes,
39}
40
41impl<P> From<Proposal<P>> for ProposalInfo {
42 fn from(p: Proposal<P>) -> Self {
43 Self {
44 title: p.title,
45 description: p.description,
46 }
47 }
48}
49
50impl<P> Proposal<P> {
51 pub fn current_status(&self, block: &BlockInfo) -> Status {
54 let mut status = self.status;
55
56 if status == Status::Open && self.is_passed(block) {
58 status = Status::Passed;
59 }
60 if status == Status::Open && self.expires.is_expired(block) {
61 status = Status::Rejected;
62 }
63
64 status
65 }
66
67 pub fn update_status(&mut self, block: &BlockInfo) {
70 self.status = self.current_status(block);
71 }
72
73 pub fn is_passed(&self, block: &BlockInfo) -> bool {
76 let VotingRules {
77 quorum,
78 threshold,
79 allow_end_early,
80 ..
81 } = self.rules;
82
83 if self.votes.total() < votes_needed(self.total_points, quorum) {
85 return false;
86 }
87 if self.expires.is_expired(block) {
88 let opinions = self.votes.total() - self.votes.abstain;
90 self.votes.yes >= votes_needed(opinions, threshold)
91 } else if allow_end_early {
92 let possible_opinions = self.total_points - self.votes.abstain;
95 self.votes.yes >= votes_needed(possible_opinions, threshold)
96 } else {
97 false
98 }
99 }
100}
101
102#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
106pub struct ProposalResponse<P> {
107 pub id: u64,
108 pub title: String,
109 pub description: String,
110 pub created_by: String,
111 pub proposal: P,
112 pub status: Status,
113 pub expires: Expiration,
114 pub rules: VotingRules,
115 pub total_points: u64,
116 pub votes: Votes,
117}
118
119#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
120pub struct ProposalListResponse<P> {
121 pub proposals: Vec<ProposalResponse<P>>,
122}
123
124#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
125pub struct TextProposalListResponse {
126 pub proposals: Vec<ProposalInfo>,
127}
128
129#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Debug, JsonSchema)]
130pub struct VotingRules {
131 pub voting_period: u32,
133 pub quorum: Decimal,
135 pub threshold: Decimal,
137 pub allow_end_early: bool,
139}
140
141impl VotingRules {
142 pub fn validate(&self) -> Result<(), ContractError> {
143 let zero = Decimal::percent(0);
144 let hundred = Decimal::percent(100);
145
146 if self.quorum == zero || self.quorum > hundred {
147 return Err(ContractError::InvalidQuorum(self.quorum));
148 }
149
150 if self.threshold < Decimal::percent(50) || self.threshold > hundred {
151 return Err(ContractError::InvalidThreshold(self.threshold));
152 }
153
154 if self.voting_period == 0 || self.voting_period > 365 {
155 return Err(ContractError::InvalidVotingPeriod(self.voting_period));
156 }
157 Ok(())
158 }
159
160 pub fn voting_period_secs(&self) -> u64 {
161 self.voting_period as u64 * 86_400
162 }
163}
164
165pub struct RulesBuilder {
166 voting_period: u32,
167 quorum: Decimal,
168 threshold: Decimal,
169 allow_end_early: bool,
170}
171
172impl RulesBuilder {
173 pub fn new() -> Self {
174 Self {
175 voting_period: 14,
176 quorum: Decimal::percent(20),
177 threshold: Decimal::percent(50),
178 allow_end_early: true,
179 }
180 }
181
182 pub fn with_threshold(mut self, threshold: impl Into<Decimal>) -> Self {
183 self.threshold = threshold.into();
184 self
185 }
186
187 pub fn with_quorum(mut self, quorum: impl Into<Decimal>) -> Self {
188 self.quorum = quorum.into();
189 self
190 }
191
192 pub fn with_allow_early(mut self, allow_end_early: bool) -> Self {
193 self.allow_end_early = allow_end_early;
194 self
195 }
196
197 pub fn build(&self) -> VotingRules {
198 VotingRules {
199 voting_period: self.voting_period,
200 quorum: self.quorum,
201 threshold: self.threshold,
202 allow_end_early: self.allow_end_early,
203 }
204 }
205}
206
207impl Default for RulesBuilder {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
215pub struct Votes {
216 pub yes: u64,
217 pub no: u64,
218 pub abstain: u64,
219 pub veto: u64,
220}
221
222impl Votes {
223 pub fn total(&self) -> u64 {
225 self.yes + self.no + self.abstain + self.veto
226 }
227
228 pub fn yes(init_points: u64) -> Self {
230 Votes {
231 yes: init_points,
232 no: 0,
233 abstain: 0,
234 veto: 0,
235 }
236 }
237
238 pub fn add_vote(&mut self, vote: Vote, points: u64) {
239 match vote {
240 Vote::Yes => self.yes += points,
241 Vote::Abstain => self.abstain += points,
242 Vote::No => self.no += points,
243 Vote::Veto => self.veto += points,
244 }
245 }
246}
247
248fn votes_needed(points: u64, percentage: Decimal) -> u64 {
251 let applied = percentage * Uint128::new(PRECISION_FACTOR * points as u128);
252 ((applied.u128() + PRECISION_FACTOR - 1) / PRECISION_FACTOR) as u64
254}
255
256pub const CONFIG: Item<Config> = Item::new("voting_config");
258pub const PROPOSAL_COUNT: Item<u64> = Item::new("proposal_count");
259
260pub fn proposals<'m, P>() -> Map<'m, u64, Proposal<P>> {
261 Map::new("proposals")
262}
263
264#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Debug)]
265pub struct ProposalInfo {
266 pub title: String,
267 pub description: String,
268}
269
270pub const TEXT_PROPOSALS: Map<u64, ProposalInfo> = Map::new("text_proposals");
271
272pub fn next_id(store: &mut dyn Storage) -> StdResult<u64> {
273 let id: u64 = PROPOSAL_COUNT.may_load(store)?.unwrap_or_default() + 1;
274 PROPOSAL_COUNT.save(store, &id)?;
275 Ok(id)
276}