penumbra_sdk_stake/
funding_stream.rs

1use crate::BPS_SQUARED_SCALING_FACTOR;
2use penumbra_sdk_keys::Address;
3use penumbra_sdk_num::{fixpoint::U128x128, Amount};
4use penumbra_sdk_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
5use serde::{Deserialize, Serialize};
6
7/// A destination for a portion of a validator's commission of staking rewards.
8#[allow(clippy::large_enum_variant)]
9#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
10#[serde(try_from = "pb::FundingStream", into = "pb::FundingStream")]
11pub enum FundingStream {
12    ToAddress {
13        /// The destination address for the funding stream..
14        address: Address,
15
16        /// The portion (in terms of [basis points](https://en.wikipedia.org/wiki/Basis_point)) of the
17        /// validator's total staking reward that goes to this funding stream.
18        rate_bps: u16,
19    },
20    ToCommunityPool {
21        /// The portion (in terms of [basis points](https://en.wikipedia.org/wiki/Basis_point)) of the
22        /// validator's total staking reward that goes to this funding stream.
23        rate_bps: u16,
24    },
25}
26
27#[allow(clippy::large_enum_variant)]
28#[derive(Debug, PartialEq, Eq, Clone)]
29pub enum Recipient {
30    Address(Address),
31    CommunityPool,
32}
33
34impl FundingStream {
35    pub fn rate_bps(&self) -> u16 {
36        match self {
37            FundingStream::ToAddress { rate_bps, .. } => *rate_bps,
38            FundingStream::ToCommunityPool { rate_bps } => *rate_bps,
39        }
40    }
41
42    pub fn recipient(&self) -> Recipient {
43        match self {
44            FundingStream::ToAddress { address, .. } => Recipient::Address(address.clone()),
45            FundingStream::ToCommunityPool { .. } => Recipient::CommunityPool,
46        }
47    }
48}
49
50impl FundingStream {
51    /// Computes the amount of reward at the epoch boundary.
52    /// The input rates are assumed to be in basis points squared, this means that
53    /// to get the actual rate, you need to rescale by [`BPS_SQUARED_SCALING_FACTOR`].
54    pub fn reward_amount(
55        &self,
56        base_reward_rate: Amount,
57        validator_exchange_rate: Amount,
58        total_delegation_tokens: Amount,
59    ) -> Amount {
60        // Setup:
61        let total_delegation_tokens = U128x128::from(total_delegation_tokens);
62        let prev_validator_exchange_rate_bps_sq = U128x128::from(validator_exchange_rate);
63        let prev_base_reward_rate_bps_sq = U128x128::from(base_reward_rate);
64        let commission_rate_bps = U128x128::from(self.rate_bps());
65        let max_bps = U128x128::from(10_000u128);
66
67        // First, we remove the scaling factors:
68        let commission_rate = (commission_rate_bps / max_bps).expect("nonzero divisor");
69        let prev_validator_exchange_rate = (prev_validator_exchange_rate_bps_sq
70            / *BPS_SQUARED_SCALING_FACTOR)
71            .expect("nonzero divisor");
72        let prev_base_reward_rate =
73            (prev_base_reward_rate_bps_sq / *BPS_SQUARED_SCALING_FACTOR).expect("nonzero divisor");
74
75        // The reward amount at epoch e, for validator v, is R_{v,e}.
76        // It is computed as:
77        //   R_{v,e} = y_v * c_{v,e} * r_e * psi_v(e)
78        //   where:
79        //          y_v = total delegation tokens for validator v
80        //          c_{v,e} = commission rate for validator v, at epoch e
81        //          r_e = base reward rate for epoch e
82        //          psi_v(e) = the validator exchange rate for epoch e
83        //
84        // The commission rate is the sum of all the funding streams rate, and is capped at 100%.
85        // In this method, we use a partial commission rate specific to `this` funding stream.
86
87        // Then, we compute the cumulative depreciation for this pool:
88        let staking_tokens = (total_delegation_tokens * prev_validator_exchange_rate)
89            .expect("exchange rate is close to 1");
90
91        // Now, we can compute the total reward amount for this pool:
92        let total_reward_amount =
93            (staking_tokens * prev_base_reward_rate).expect("does not overflow");
94
95        /* ********** Compute the reward amount for this funding stream ************* */
96        let stream_reward_amount =
97            (total_reward_amount * commission_rate).expect("commission rate is between 0 and 1");
98        /* ************************************************************************** */
99
100        stream_reward_amount
101            .round_down()
102            .try_into()
103            .expect("does not overflow")
104    }
105}
106
107impl DomainType for FundingStream {
108    type Proto = pb::FundingStream;
109}
110
111impl From<FundingStream> for pb::FundingStream {
112    fn from(fs: FundingStream) -> Self {
113        pb::FundingStream {
114            recipient: match fs {
115                FundingStream::ToAddress { address, rate_bps } => Some(
116                    pb::funding_stream::Recipient::ToAddress(pb::funding_stream::ToAddress {
117                        address: address.to_string(),
118                        rate_bps: rate_bps.into(),
119                    }),
120                ),
121                FundingStream::ToCommunityPool { rate_bps } => {
122                    Some(pb::funding_stream::Recipient::ToCommunityPool(
123                        pb::funding_stream::ToCommunityPool {
124                            rate_bps: rate_bps.into(),
125                        },
126                    ))
127                }
128            },
129        }
130    }
131}
132
133impl TryFrom<pb::FundingStream> for FundingStream {
134    type Error = anyhow::Error;
135
136    fn try_from(fs: pb::FundingStream) -> Result<Self, Self::Error> {
137        match fs
138            .recipient
139            .ok_or_else(|| anyhow::anyhow!("missing funding stream recipient"))?
140        {
141            pb::funding_stream::Recipient::ToAddress(to_address) => {
142                let address = to_address
143                    .address
144                    .parse()
145                    .map_err(|e| anyhow::anyhow!("invalid funding stream address: {}", e))?;
146                let rate_bps = to_address
147                    .rate_bps
148                    .try_into()
149                    .map_err(|e| anyhow::anyhow!("invalid funding stream rate: {}", e))?;
150                if rate_bps > 10_000 {
151                    anyhow::bail!("funding stream rate exceeds 100% (10,000bps)");
152                }
153                Ok(FundingStream::ToAddress { address, rate_bps })
154            }
155            pb::funding_stream::Recipient::ToCommunityPool(to_community_pool) => {
156                let rate_bps = to_community_pool
157                    .rate_bps
158                    .try_into()
159                    .map_err(|e| anyhow::anyhow!("invalid funding stream rate: {}", e))?;
160                if rate_bps > 10_000 {
161                    anyhow::bail!("funding stream rate exceeds 100% (10,000bps)");
162                }
163                Ok(FundingStream::ToCommunityPool { rate_bps })
164            }
165        }
166    }
167}
168
169/// A list of funding streams whose total commission is less than 100%.
170///
171/// The total commission of a validator is the sum of the individual reward rate of the
172/// [`FundingStream`]s, and cannot exceed 10000bps (100%). This property is guaranteed by the
173/// `TryFrom<Vec<FundingStream>` implementation for [`FundingStreams`], which checks the sum, and is
174/// the only way to build a non-empty [`FundingStreams`].
175///
176/// Similarly, it's not possible to build a [`FundingStreams`] with more than 8 funding streams.
177#[derive(Debug, Clone, Default, Eq, PartialEq)]
178pub struct FundingStreams {
179    funding_streams: Vec<FundingStream>,
180}
181
182impl FundingStreams {
183    pub fn new() -> Self {
184        Self {
185            funding_streams: Vec::new(),
186        }
187    }
188
189    pub fn iter(&self) -> impl Iterator<Item = &FundingStream> {
190        self.funding_streams.iter()
191    }
192
193    pub fn len(&self) -> usize {
194        self.funding_streams.len()
195    }
196}
197
198impl TryFrom<Vec<FundingStream>> for FundingStreams {
199    type Error = anyhow::Error;
200
201    fn try_from(funding_streams: Vec<FundingStream>) -> Result<Self, Self::Error> {
202        if funding_streams.len() > 8 {
203            anyhow::bail!("validators can declare at most 8 funding streams");
204        }
205
206        if funding_streams.iter().map(|fs| fs.rate_bps()).sum::<u16>() > 10_000 {
207            anyhow::bail!("sum of funding rates exceeds 100% (10,000bps)");
208        }
209
210        Ok(Self { funding_streams })
211    }
212}
213
214impl From<FundingStreams> for Vec<FundingStream> {
215    fn from(funding_streams: FundingStreams) -> Self {
216        funding_streams.funding_streams
217    }
218}
219
220impl AsRef<[FundingStream]> for FundingStreams {
221    fn as_ref(&self) -> &[FundingStream] {
222        &self.funding_streams
223    }
224}
225
226impl IntoIterator for FundingStreams {
227    type Item = FundingStream;
228    type IntoIter = std::vec::IntoIter<FundingStream>;
229
230    fn into_iter(self) -> Self::IntoIter {
231        self.funding_streams.into_iter()
232    }
233}
234
235impl<'a> IntoIterator for &'a FundingStreams {
236    type Item = &'a FundingStream;
237    type IntoIter = std::slice::Iter<'a, FundingStream>;
238
239    fn into_iter(self) -> Self::IntoIter {
240        (self.funding_streams).iter()
241    }
242}