1use serde::{Deserialize, Serialize};
2use std::{
3 cmp::Ordering,
4 fmt::{self, Display, Formatter},
5 ops::{Add, AddAssign},
6 str::FromStr,
7};
8
9use penumbra_sdk_proto::{penumbra::core::component::governance::v1 as pb, DomainType};
10
11use crate::{
12 params::GovernanceParameters,
13 proposal_state::{Outcome as StateOutcome, Withdrawn},
14 vote::Vote,
15};
16
17#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(try_from = "pb::Tally", into = "pb::Tally")]
19pub struct Tally {
20 yes: u64,
21 no: u64,
22 abstain: u64,
23}
24
25impl Tally {
26 pub fn yes(&self) -> u64 {
27 self.yes
28 }
29
30 pub fn no(&self) -> u64 {
31 self.no
32 }
33
34 pub fn abstain(&self) -> u64 {
35 self.abstain
36 }
37
38 pub fn total(&self) -> u64 {
39 self.yes + self.no + self.abstain
40 }
41}
42
43impl From<Tally> for pb::Tally {
44 fn from(tally: Tally) -> Self {
45 Self {
46 yes: tally.yes,
47 no: tally.no,
48 abstain: tally.abstain,
49 }
50 }
51}
52
53impl From<pb::Tally> for Tally {
54 fn from(tally: pb::Tally) -> Self {
55 Self {
56 yes: tally.yes,
57 no: tally.no,
58 abstain: tally.abstain,
59 }
60 }
61}
62
63impl DomainType for Tally {
64 type Proto = pb::Tally;
65}
66
67impl From<(Vote, u64)> for Tally {
68 fn from((vote, power): (Vote, u64)) -> Self {
69 let mut tally = Self::default();
70 *match vote {
71 Vote::Yes => &mut tally.yes,
72 Vote::No => &mut tally.no,
73 Vote::Abstain => &mut tally.abstain,
74 } = power;
75 tally
76 }
77}
78
79impl From<(u64, Vote)> for Tally {
80 fn from((power, vote): (u64, Vote)) -> Self {
81 Self::from((vote, power))
82 }
83}
84
85impl Add for Tally {
86 type Output = Self;
87
88 fn add(self, rhs: Self) -> Self::Output {
89 Self {
90 yes: self.yes + rhs.yes,
91 no: self.no + rhs.no,
92 abstain: self.abstain + rhs.abstain,
93 }
94 }
95}
96
97impl AddAssign for Tally {
98 fn add_assign(&mut self, rhs: Self) {
99 self.yes += rhs.yes;
100 self.no += rhs.no;
101 self.abstain += rhs.abstain;
102 }
103}
104
105#[derive(Clone, Copy, Debug, PartialEq, Eq)]
106pub enum Outcome {
107 Pass,
108 Fail,
109 Slash,
110}
111
112impl Outcome {
113 pub fn is_pass(&self) -> bool {
114 matches!(self, Self::Pass)
115 }
116
117 pub fn is_fail(&self) -> bool {
118 matches!(self, Self::Fail)
119 }
120
121 pub fn is_slash(&self) -> bool {
122 matches!(self, Self::Slash)
123 }
124}
125
126impl<T> From<Outcome> for StateOutcome<T> {
127 fn from(outcome: Outcome) -> Self {
128 match outcome {
129 Outcome::Pass => Self::Passed,
130 Outcome::Fail => Self::Failed {
131 withdrawn: Withdrawn::No,
132 },
133 Outcome::Slash => Self::Slashed {
134 withdrawn: Withdrawn::No,
135 },
136 }
137 }
138}
139
140impl Tally {
141 fn meets_quorum(&self, total_voting_power: u64, params: &GovernanceParameters) -> bool {
142 Ratio::new(self.total(), total_voting_power) >= params.proposal_valid_quorum
143 }
144
145 fn slashed(&self, params: &GovernanceParameters) -> bool {
146 Ratio::new(self.no, self.total()) > params.proposal_slash_threshold
147 }
148
149 fn yes_ratio(&self) -> Ratio {
150 Ratio::new(self.yes, (self.yes + self.no).min(1))
151 }
155
156 pub fn outcome(self, total_voting_power: u64, params: &GovernanceParameters) -> Outcome {
157 use Outcome::*;
158
159 if !self.meets_quorum(total_voting_power, params) {
161 return Fail;
162 }
163
164 if self.slashed(params) {
166 return Slash;
167 }
168
169 if self.yes_ratio() > params.proposal_pass_threshold {
171 Pass
172 } else {
173 Fail
174 }
175 }
176
177 pub fn emergency_pass(self, total_voting_power: u64, params: &GovernanceParameters) -> bool {
178 if !self.meets_quorum(total_voting_power, params) {
180 return false;
181 }
182
183 if self.slashed(params) {
185 return false;
186 }
187
188 Ratio::new(self.yes, total_voting_power) > Ratio::new(1, 3)
191 }
192}
193
194#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
198#[serde(try_from = "pb::Ratio", into = "pb::Ratio")]
199pub struct Ratio {
200 numerator: u64,
201 denominator: u64,
202}
203
204impl Display for Ratio {
205 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
206 write!(f, "{}/{}", self.numerator, self.denominator)
207 }
208}
209
210impl FromStr for Ratio {
211 type Err = anyhow::Error;
212
213 fn from_str(s: &str) -> Result<Self, Self::Err> {
214 let mut parts = s.split('/');
215 let numerator = parts
216 .next()
217 .ok_or_else(|| anyhow::anyhow!("missing numerator"))?
218 .parse()?;
219 let denominator = parts
220 .next()
221 .ok_or_else(|| anyhow::anyhow!("missing denominator"))?
222 .parse()?;
223 if parts.next().is_some() {
224 anyhow::bail!("too many parts");
225 }
226 Ok(Ratio {
227 numerator,
228 denominator,
229 })
230 }
231}
232
233impl Ratio {
234 pub fn new(numerator: u64, denominator: u64) -> Self {
235 Self {
236 numerator,
237 denominator,
238 }
239 }
240}
241
242impl PartialEq for Ratio {
243 fn eq(&self, other: &Self) -> bool {
244 u128::from(self.numerator) * u128::from(other.denominator)
246 == u128::from(self.denominator) * u128::from(other.numerator)
247 }
248}
249
250impl Eq for Ratio {}
251
252impl PartialOrd for Ratio {
253 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
254 Some(self.cmp(other))
255 }
256}
257
258impl Ord for Ratio {
259 fn cmp(&self, other: &Self) -> Ordering {
260 (u128::from(self.numerator) * u128::from(other.denominator))
262 .cmp(&(u128::from(self.denominator) * u128::from(other.numerator)))
263 }
264}
265
266impl From<Ratio> for pb::Ratio {
267 fn from(ratio: Ratio) -> Self {
268 pb::Ratio {
269 numerator: ratio.numerator,
270 denominator: ratio.denominator,
271 }
272 }
273}
274
275impl From<pb::Ratio> for Ratio {
276 fn from(msg: pb::Ratio) -> Self {
277 Ratio {
278 numerator: msg.numerator,
279 denominator: msg.denominator,
280 }
281 }
282}