penumbra_sdk_stake/
funding_stream.rs1use 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#[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 address: Address,
15
16 rate_bps: u16,
19 },
20 ToCommunityPool {
21 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 pub fn reward_amount(
55 &self,
56 base_reward_rate: Amount,
57 validator_exchange_rate: Amount,
58 total_delegation_tokens: Amount,
59 ) -> Amount {
60 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 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 let staking_tokens = (total_delegation_tokens * prev_validator_exchange_rate)
89 .expect("exchange rate is close to 1");
90
91 let total_reward_amount =
93 (staking_tokens * prev_base_reward_rate).expect("does not overflow");
94
95 let stream_reward_amount =
97 (total_reward_amount * commission_rate).expect("commission rate is between 0 and 1");
98 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#[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}