penumbra_sdk_governance/
tally.rs

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        // ^ in the above, the `.min(1)` is to prevent a divide-by-zero error when the only votes
152        // cast are abstains -- this results in a 0:1 ratio in that case, which will never pass, as
153        // desired in that situation
154    }
155
156    pub fn outcome(self, total_voting_power: u64, params: &GovernanceParameters) -> Outcome {
157        use Outcome::*;
158
159        // Check to see if we've met quorum
160        if !self.meets_quorum(total_voting_power, params) {
161            return Fail;
162        }
163
164        // Check to see if it has been slashed
165        if self.slashed(params) {
166            return Slash;
167        }
168
169        // Now that we've checked for slash and quorum, we can just check to see if it should pass
170        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        // Check to see if we've met quorum
179        if !self.meets_quorum(total_voting_power, params) {
180            return false;
181        }
182
183        // Check to see if it has been slashed (this check should be redundant, but we'll do it anyway)
184        if self.slashed(params) {
185            return false;
186        }
187
188        // Now that we've checked for slash and quorum, we can just check to see if it should pass in
189        // the emergency condition of 1/3 majority of voting power
190        Ratio::new(self.yes, total_voting_power) > Ratio::new(1, 3)
191    }
192}
193
194/// This is a ratio of two `u64` values, intended to be used solely in governance parameters and
195/// tallying. It only implements construction and comparison, not arithmetic, to reduce the trusted
196/// codebase for governance.
197#[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        // Convert everything to `u128` to avoid overflow when multiplying
245        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        // Convert everything to `u128` to avoid overflow when multiplying
261        (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}