penumbra_sdk_stake/
rate.rs

1//! Staking reward and delegation token exchange rates.
2
3use penumbra_sdk_num::fixpoint::U128x128;
4use penumbra_sdk_num::Amount;
5use penumbra_sdk_proto::core::component::stake::v1::CurrentValidatorRateResponse;
6use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
7use penumbra_sdk_sct::epoch::Epoch;
8use serde::{Deserialize, Serialize};
9
10use crate::{validator::State, FundingStream, IdentityKey};
11use crate::{Delegate, Penalty, Undelegate, BPS_SQUARED_SCALING_FACTOR};
12
13/// Describes a validator's reward rate and voting power in some epoch.
14#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
15#[serde(try_from = "pb::RateData", into = "pb::RateData")]
16pub struct RateData {
17    /// The validator's identity key.
18    pub identity_key: IdentityKey,
19    /// The validator-specific reward rate.
20    pub validator_reward_rate: Amount,
21    /// The validator-specific exchange rate.
22    pub validator_exchange_rate: Amount,
23}
24
25impl RateData {
26    /// Compute the validator rate data for the next epoch.
27    ///
28    /// # Panics
29    /// This method panics if the validator's funding streams exceed 100%.
30    /// The stateless checks in the [`Definition`](crate::validator::Definition) action handler
31    /// should prevent this from happening.
32    pub fn next_epoch(
33        &self,
34        next_base_rate: &BaseRateData,
35        funding_streams: &[FundingStream],
36        validator_state: &State,
37    ) -> RateData {
38        let previous_rate = self;
39
40        if let State::Active = validator_state {
41            // Compute the validator's total commission rate in basis points.
42            let validator_commission_bps = funding_streams
43                .iter()
44                .fold(0u64, |total, stream| total + stream.rate_bps() as u64);
45
46            if validator_commission_bps > 1_0000 {
47                // We should never hit this branch: validator funding streams should be verified not to
48                // sum past 100% in the state machine's validation of registration of new funding
49                // streams
50                panic!("commission rate sums to > 100%")
51            }
52
53            // Rate data is represented with an implicit scaling factor of 1_0000_0000.
54            // To make the calculations more readable, we use `U128x128` to represent
55            // the intermediate descaled values. As a last step, we scaled them back
56            // using [`BPS_SQUARED_SCALING_FACTOR`] and round down to an [`Amount`].
57
58            /* Setting up constants and unrolling scaling factors */
59            let one = U128x128::from(1u128);
60            let max_bps = U128x128::from(1_0000u128);
61
62            let validator_commission_bps = U128x128::from(validator_commission_bps);
63            let next_base_reward_rate = U128x128::from(next_base_rate.base_reward_rate);
64            let previous_validator_exchange_rate =
65                U128x128::from(previous_rate.validator_exchange_rate);
66
67            let validator_commission =
68                (validator_commission_bps / max_bps).expect("max_bps is nonzero");
69            let next_base_reward_rate = (next_base_reward_rate / *BPS_SQUARED_SCALING_FACTOR)
70                .expect("scaling factor is nonzero");
71            let previous_validator_exchange_rate = (previous_validator_exchange_rate
72                / *BPS_SQUARED_SCALING_FACTOR)
73                .expect("scaling factor is nonzero");
74            /* ************************************************* */
75
76            /* ************ Compute the validator reward rate **************** */
77            tracing::debug!(%validator_commission, %next_base_reward_rate, "computing validator reward rate");
78            let commission_factor =
79                (one - validator_commission).expect("0 <= validator_commission_bps <= 1");
80            tracing::debug!(%commission_factor, "complement commission rate");
81
82            let next_validator_reward_rate =
83                (next_base_reward_rate * commission_factor).expect("does not overflow");
84            tracing::debug!(%next_validator_reward_rate, "validator reward rate");
85            /* ***************************************************************** */
86
87            /* ************ Compute the validator exchange rate **************** */
88            tracing::debug!(%next_validator_reward_rate, %previous_validator_exchange_rate, "computing validator exchange rate");
89
90            let reward_growth_factor =
91                (one + next_validator_reward_rate).expect("does not overflow");
92            let next_validator_exchange_rate = (previous_validator_exchange_rate
93                * reward_growth_factor)
94                .expect("does not overflow");
95            tracing::debug!(%next_validator_exchange_rate, "computed the validator exchange rate");
96            /* ***************************************************************** */
97
98            /* Rescale the rate data using the fixed point scaling factor */
99            let next_validator_reward_rate = (next_validator_reward_rate
100                * *BPS_SQUARED_SCALING_FACTOR)
101                .expect("rate is between 0 and 1")
102                .round_down()
103                .try_into()
104                .expect("rounding down gives an integral type");
105            let next_validator_exchange_rate = (next_validator_exchange_rate
106                * *BPS_SQUARED_SCALING_FACTOR)
107                .expect("rate is between 0 and 1")
108                .round_down()
109                .try_into()
110                .expect("rounding down gives an integral type");
111            /* ************************************************************* */
112
113            RateData {
114                identity_key: previous_rate.identity_key.clone(),
115                validator_reward_rate: next_validator_reward_rate,
116                validator_exchange_rate: next_validator_exchange_rate,
117            }
118        } else {
119            // Non-Active validator states result in a constant rate. This means
120            // the next epoch's rate is set to the current rate.
121            RateData {
122                identity_key: previous_rate.identity_key.clone(),
123                validator_reward_rate: previous_rate.validator_reward_rate,
124                validator_exchange_rate: previous_rate.validator_exchange_rate,
125            }
126        }
127    }
128
129    /// Computes the amount of delegation tokens corresponding to the given amount of unbonded stake.
130    ///
131    /// # Warning
132    ///
133    /// Given a pair `(delegation_amount, unbonded_amount)` and `rate_data`, it's possible to have
134    /// ```rust,ignore
135    /// delegation_amount == rate_data.delegation_amount(unbonded_amount)
136    /// ```
137    /// or
138    /// ```rust,ignore
139    /// unbonded_amount == rate_data.unbonded_amount(delegation_amount)
140    /// ```
141    /// but in general *not both*, because the computation involves rounding.
142    pub fn delegation_amount(&self, unbonded_amount: Amount) -> Amount {
143        // Setup:
144        let unbonded_amount = U128x128::from(unbonded_amount);
145        let validator_exchange_rate = U128x128::from(self.validator_exchange_rate);
146
147        // Remove scaling factors:
148        let validator_exchange_rate = (validator_exchange_rate / *BPS_SQUARED_SCALING_FACTOR)
149            .expect("scaling factor is nonzero");
150        if validator_exchange_rate == U128x128::from(0u128) {
151            // If the exchange rate is zero, the delegation amount is also zero.
152            // This is extremely unlikely to be hit in practice, but it's a valid
153            // edge case that a test might want to cover.
154            return 0u128.into();
155        }
156
157        /* **************** Compute the corresponding delegation size *********************** */
158
159        let delegation_amount = (unbonded_amount / validator_exchange_rate)
160            .expect("validator exchange rate is nonzero");
161        /* ********************************************************************************** */
162
163        delegation_amount
164            .round_down()
165            .try_into()
166            .expect("rounding down gives an integral type")
167    }
168
169    pub fn slash(&self, penalty: Penalty) -> Self {
170        let mut slashed = self.clone();
171        // This will automatically produce a ratio which is multiplied by 1_0000_0000, and so
172        // rounding down does what we want.
173        let penalized_exchange_rate: Amount = penalty
174            .apply_to(self.validator_exchange_rate)
175            .round_down()
176            .try_into()
177            .expect("multiplying will not overflow");
178        slashed.validator_exchange_rate = penalized_exchange_rate;
179        slashed
180    }
181
182    /// Computes the amount of unbonded stake corresponding to the given amount of delegation tokens.
183    ///
184    /// # Warning
185    ///
186    /// Given a pair `(delegation_amount, unbonded_amount)` and `rate_data`, it's possible to have
187    /// ```rust,ignore
188    /// delegation_amount == rate_data.delegation_amount(unbonded_amount)
189    /// ```
190    /// or
191    /// ```rust,ignore
192    /// unbonded_amount == rate_data.unbonded_amount(delegation_amount)
193    /// ```
194    /// but in general *not both*, because the computation involves rounding.
195    pub fn unbonded_amount(&self, delegation_amount: Amount) -> Amount {
196        // Setup:
197        let delegation_amount = U128x128::from(delegation_amount);
198        let validator_exchange_rate = U128x128::from(self.validator_exchange_rate);
199
200        // Remove scaling factors:
201        let validator_exchange_rate = (validator_exchange_rate / *BPS_SQUARED_SCALING_FACTOR)
202            .expect("scaling factor is nonzero");
203
204        /* **************** Compute the unbonded amount *********************** */
205        (delegation_amount * validator_exchange_rate)
206            .expect("does not overflow")
207            .round_down()
208            .try_into()
209            .expect("rounding down gives an integral type")
210    }
211
212    /// Compute the voting power of the validator given the size of its delegation pool.
213    pub fn voting_power(&self, delegation_pool_size: Amount) -> Amount {
214        // Setup:
215        let delegation_pool_size = U128x128::from(delegation_pool_size);
216        let validator_exchange_rate = U128x128::from(self.validator_exchange_rate);
217
218        // Remove scaling factors:
219        let validator_exchange_rate = (validator_exchange_rate / *BPS_SQUARED_SCALING_FACTOR)
220            .expect("scaling factor is nonzero");
221
222        /* ************************ Convert the delegation tokens to staking tokens ******************** */
223        let voting_power = (delegation_pool_size * validator_exchange_rate)
224            .expect("does not overflow")
225            .round_down()
226            .try_into()
227            .expect("rounding down gives an integral type");
228        /* ******************************************************************************************* */
229
230        voting_power
231    }
232
233    /// Uses this `RateData` to build a `Delegate` transaction action that
234    /// delegates `unbonded_amount` of the staking token.
235    pub fn build_delegate(&self, epoch: Epoch, unbonded_amount: Amount) -> Delegate {
236        Delegate {
237            delegation_amount: self.delegation_amount(unbonded_amount),
238            epoch_index: epoch.index,
239            unbonded_amount,
240            validator_identity: self.identity_key.clone(),
241        }
242    }
243
244    /// Uses this `RateData` to build an `Undelegate` transaction action that
245    /// undelegates `delegation_amount` of the validator's delegation tokens.
246    pub fn build_undelegate(&self, start_epoch: Epoch, delegation_amount: Amount) -> Undelegate {
247        Undelegate {
248            from_epoch: start_epoch,
249            delegation_amount,
250            unbonded_amount: self.unbonded_amount(delegation_amount),
251            validator_identity: self.identity_key.clone(),
252        }
253    }
254}
255
256/// Describes the base reward and exchange rates in some epoch.
257#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
258#[serde(try_from = "pb::BaseRateData", into = "pb::BaseRateData")]
259pub struct BaseRateData {
260    /// The index of the epoch for which this rate is valid.
261    pub epoch_index: u64,
262    /// The base reward rate.
263    pub base_reward_rate: Amount,
264    /// The base exchange rate.
265    pub base_exchange_rate: Amount,
266}
267
268impl BaseRateData {
269    /// Compute the next epoch's base rate.
270    pub fn next_epoch(&self, next_base_reward_rate: Amount) -> BaseRateData {
271        // Setup:
272        let prev_base_exchange_rate = U128x128::from(self.base_exchange_rate);
273        let next_base_reward_rate_scaled = next_base_reward_rate.clone();
274        let next_base_reward_rate = U128x128::from(next_base_reward_rate);
275        let one = U128x128::from(1u128);
276
277        // Remove scaling factors:
278        let prev_base_exchange_rate = (prev_base_exchange_rate / *BPS_SQUARED_SCALING_FACTOR)
279            .expect("scaling factor is nonzero");
280        let next_base_reward_rate_fp = (next_base_reward_rate / *BPS_SQUARED_SCALING_FACTOR)
281            .expect("scaling factor is nonzero");
282
283        // Compute the reward growth factor:
284        let reward_growth_factor = (one + next_base_reward_rate_fp).expect("does not overflow");
285
286        /* ********* Compute the base exchange rate for the next epoch ****************** */
287        let next_base_exchange_rate =
288            (prev_base_exchange_rate * reward_growth_factor).expect("does not overflow");
289        /* ****************************************************************************** */
290
291        // Rescale the exchange rate:
292        let next_base_exchange_rate_scaled = (next_base_exchange_rate
293            * *BPS_SQUARED_SCALING_FACTOR)
294            .expect("rate is between 0 and 1")
295            .round_down()
296            .try_into()
297            .expect("rounding down gives an integral type");
298
299        BaseRateData {
300            base_exchange_rate: next_base_exchange_rate_scaled,
301            base_reward_rate: next_base_reward_rate_scaled,
302            epoch_index: self.epoch_index + 1,
303        }
304    }
305}
306
307impl DomainType for RateData {
308    type Proto = pb::RateData;
309}
310
311impl From<RateData> for pb::RateData {
312    #[allow(deprecated)]
313    fn from(v: RateData) -> Self {
314        pb::RateData {
315            identity_key: Some(v.identity_key.into()),
316            epoch_index: 0,
317            validator_reward_rate: Some(v.validator_reward_rate.into()),
318            validator_exchange_rate: Some(v.validator_exchange_rate.into()),
319        }
320    }
321}
322
323impl TryFrom<pb::RateData> for RateData {
324    type Error = anyhow::Error;
325    fn try_from(v: pb::RateData) -> Result<Self, Self::Error> {
326        Ok(RateData {
327            identity_key: v
328                .identity_key
329                .ok_or_else(|| anyhow::anyhow!("missing identity key"))?
330                .try_into()?,
331            validator_reward_rate: v
332                .validator_reward_rate
333                .ok_or_else(|| anyhow::anyhow!("empty validator reward rate in RateData message"))?
334                .try_into()?,
335            validator_exchange_rate: v
336                .validator_exchange_rate
337                .ok_or_else(|| {
338                    anyhow::anyhow!("empty validator exchange rate in RateData message")
339                })?
340                .try_into()?,
341        })
342    }
343}
344
345impl DomainType for BaseRateData {
346    type Proto = pb::BaseRateData;
347}
348
349impl From<BaseRateData> for pb::BaseRateData {
350    fn from(rate: BaseRateData) -> Self {
351        pb::BaseRateData {
352            epoch_index: rate.epoch_index,
353            base_reward_rate: Some(rate.base_reward_rate.into()),
354            base_exchange_rate: Some(rate.base_exchange_rate.into()),
355        }
356    }
357}
358
359impl TryFrom<pb::BaseRateData> for BaseRateData {
360    type Error = anyhow::Error;
361    fn try_from(v: pb::BaseRateData) -> Result<Self, Self::Error> {
362        Ok(BaseRateData {
363            epoch_index: v.epoch_index,
364            base_reward_rate: v
365                .base_reward_rate
366                .ok_or_else(|| anyhow::anyhow!("empty base reward rate in BaseRateData message"))?
367                .try_into()?,
368            base_exchange_rate: v
369                .base_exchange_rate
370                .ok_or_else(|| anyhow::anyhow!("empty base exchange rate in BaseRateData message"))?
371                .try_into()?,
372        })
373    }
374}
375
376impl From<RateData> for CurrentValidatorRateResponse {
377    fn from(r: RateData) -> Self {
378        CurrentValidatorRateResponse {
379            data: Some(r.into()),
380        }
381    }
382}
383
384impl TryFrom<CurrentValidatorRateResponse> for RateData {
385    type Error = anyhow::Error;
386
387    fn try_from(value: CurrentValidatorRateResponse) -> Result<Self, Self::Error> {
388        value
389            .data
390            .ok_or_else(|| anyhow::anyhow!("empty CurrentValidatorRateResponse message"))?
391            .try_into()
392    }
393}
394
395#[cfg(test)]
396mod tests {
397    use super::*;
398    use decaf377_rdsa as rdsa;
399    use rand_core::OsRng;
400
401    #[test]
402    fn slash_rate_by_penalty() {
403        let vk = rdsa::VerificationKey::from(rdsa::SigningKey::new(OsRng));
404        let ik = IdentityKey(vk.into());
405
406        let rate_data = RateData {
407            identity_key: ik,
408            validator_reward_rate: 1_0000_0000u128.into(),
409            validator_exchange_rate: 2_0000_0000u128.into(),
410        };
411        // 10%
412        let penalty = Penalty::from_percent(10);
413        let slashed = rate_data.slash(penalty);
414        assert_eq!(slashed.validator_exchange_rate, 1_8000_0000u128.into());
415    }
416}