penumbra_sdk_stake/
rate.rs1use 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#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
15#[serde(try_from = "pb::RateData", into = "pb::RateData")]
16pub struct RateData {
17 pub identity_key: IdentityKey,
19 pub validator_reward_rate: Amount,
21 pub validator_exchange_rate: Amount,
23}
24
25impl RateData {
26 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 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 panic!("commission rate sums to > 100%")
51 }
52
53 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 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 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 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 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 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 pub fn delegation_amount(&self, unbonded_amount: Amount) -> Amount {
143 let unbonded_amount = U128x128::from(unbonded_amount);
145 let validator_exchange_rate = U128x128::from(self.validator_exchange_rate);
146
147 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 return 0u128.into();
155 }
156
157 let delegation_amount = (unbonded_amount / validator_exchange_rate)
160 .expect("validator exchange rate is nonzero");
161 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 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 pub fn unbonded_amount(&self, delegation_amount: Amount) -> Amount {
196 let delegation_amount = U128x128::from(delegation_amount);
198 let validator_exchange_rate = U128x128::from(self.validator_exchange_rate);
199
200 let validator_exchange_rate = (validator_exchange_rate / *BPS_SQUARED_SCALING_FACTOR)
202 .expect("scaling factor is nonzero");
203
204 (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 pub fn voting_power(&self, delegation_pool_size: Amount) -> Amount {
214 let delegation_pool_size = U128x128::from(delegation_pool_size);
216 let validator_exchange_rate = U128x128::from(self.validator_exchange_rate);
217
218 let validator_exchange_rate = (validator_exchange_rate / *BPS_SQUARED_SCALING_FACTOR)
220 .expect("scaling factor is nonzero");
221
222 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 voting_power
231 }
232
233 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 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#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
258#[serde(try_from = "pb::BaseRateData", into = "pb::BaseRateData")]
259pub struct BaseRateData {
260 pub epoch_index: u64,
262 pub base_reward_rate: Amount,
264 pub base_exchange_rate: Amount,
266}
267
268impl BaseRateData {
269 pub fn next_epoch(&self, next_base_reward_rate: Amount) -> BaseRateData {
271 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 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 let reward_growth_factor = (one + next_base_reward_rate_fp).expect("does not overflow");
285
286 let next_base_exchange_rate =
288 (prev_base_exchange_rate * reward_growth_factor).expect("does not overflow");
289 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 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}