penumbra_stake/
funding_stream.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
use crate::BPS_SQUARED_SCALING_FACTOR;
use penumbra_keys::Address;
use penumbra_num::{fixpoint::U128x128, Amount};
use penumbra_proto::{penumbra::core::component::stake::v1 as pb, DomainType};
use serde::{Deserialize, Serialize};

/// A destination for a portion of a validator's commission of staking rewards.
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, Clone)]
#[serde(try_from = "pb::FundingStream", into = "pb::FundingStream")]
pub enum FundingStream {
    ToAddress {
        /// The destination address for the funding stream..
        address: Address,

        /// The portion (in terms of [basis points](https://en.wikipedia.org/wiki/Basis_point)) of the
        /// validator's total staking reward that goes to this funding stream.
        rate_bps: u16,
    },
    ToCommunityPool {
        /// The portion (in terms of [basis points](https://en.wikipedia.org/wiki/Basis_point)) of the
        /// validator's total staking reward that goes to this funding stream.
        rate_bps: u16,
    },
}

#[allow(clippy::large_enum_variant)]
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum Recipient {
    Address(Address),
    CommunityPool,
}

impl FundingStream {
    pub fn rate_bps(&self) -> u16 {
        match self {
            FundingStream::ToAddress { rate_bps, .. } => *rate_bps,
            FundingStream::ToCommunityPool { rate_bps } => *rate_bps,
        }
    }

    pub fn recipient(&self) -> Recipient {
        match self {
            FundingStream::ToAddress { address, .. } => Recipient::Address(address.clone()),
            FundingStream::ToCommunityPool { .. } => Recipient::CommunityPool,
        }
    }
}

impl FundingStream {
    /// Computes the amount of reward at the epoch boundary.
    /// The input rates are assumed to be in basis points squared, this means that
    /// to get the actual rate, you need to rescale by [`BPS_SQUARED_SCALING_FACTOR`].
    pub fn reward_amount(
        &self,
        base_reward_rate: Amount,
        validator_exchange_rate: Amount,
        total_delegation_tokens: Amount,
    ) -> Amount {
        // Setup:
        let total_delegation_tokens = U128x128::from(total_delegation_tokens);
        let prev_validator_exchange_rate_bps_sq = U128x128::from(validator_exchange_rate);
        let prev_base_reward_rate_bps_sq = U128x128::from(base_reward_rate);
        let commission_rate_bps = U128x128::from(self.rate_bps());
        let max_bps = U128x128::from(10_000u128);

        // First, we remove the scaling factors:
        let commission_rate = (commission_rate_bps / max_bps).expect("nonzero divisor");
        let prev_validator_exchange_rate = (prev_validator_exchange_rate_bps_sq
            / *BPS_SQUARED_SCALING_FACTOR)
            .expect("nonzero divisor");
        let prev_base_reward_rate =
            (prev_base_reward_rate_bps_sq / *BPS_SQUARED_SCALING_FACTOR).expect("nonzero divisor");

        // The reward amount at epoch e, for validator v, is R_{v,e}.
        // It is computed as:
        //   R_{v,e} = y_v * c_{v,e} * r_e * psi_v(e)
        //   where:
        //          y_v = total delegation tokens for validator v
        //          c_{v,e} = commission rate for validator v, at epoch e
        //          r_e = base reward rate for epoch e
        //          psi_v(e) = the validator exchange rate for epoch e
        //
        // The commission rate is the sum of all the funding streams rate, and is capped at 100%.
        // In this method, we use a partial commission rate specific to `this` funding stream.

        // Then, we compute the cumulative depreciation for this pool:
        let staking_tokens = (total_delegation_tokens * prev_validator_exchange_rate)
            .expect("exchange rate is close to 1");

        // Now, we can compute the total reward amount for this pool:
        let total_reward_amount =
            (staking_tokens * prev_base_reward_rate).expect("does not overflow");

        /* ********** Compute the reward amount for this funding stream ************* */
        let stream_reward_amount =
            (total_reward_amount * commission_rate).expect("commission rate is between 0 and 1");
        /* ************************************************************************** */

        stream_reward_amount
            .round_down()
            .try_into()
            .expect("does not overflow")
    }
}

impl DomainType for FundingStream {
    type Proto = pb::FundingStream;
}

impl From<FundingStream> for pb::FundingStream {
    fn from(fs: FundingStream) -> Self {
        pb::FundingStream {
            recipient: match fs {
                FundingStream::ToAddress { address, rate_bps } => Some(
                    pb::funding_stream::Recipient::ToAddress(pb::funding_stream::ToAddress {
                        address: address.to_string(),
                        rate_bps: rate_bps.into(),
                    }),
                ),
                FundingStream::ToCommunityPool { rate_bps } => {
                    Some(pb::funding_stream::Recipient::ToCommunityPool(
                        pb::funding_stream::ToCommunityPool {
                            rate_bps: rate_bps.into(),
                        },
                    ))
                }
            },
        }
    }
}

impl TryFrom<pb::FundingStream> for FundingStream {
    type Error = anyhow::Error;

    fn try_from(fs: pb::FundingStream) -> Result<Self, Self::Error> {
        match fs
            .recipient
            .ok_or_else(|| anyhow::anyhow!("missing funding stream recipient"))?
        {
            pb::funding_stream::Recipient::ToAddress(to_address) => {
                let address = to_address
                    .address
                    .parse()
                    .map_err(|e| anyhow::anyhow!("invalid funding stream address: {}", e))?;
                let rate_bps = to_address
                    .rate_bps
                    .try_into()
                    .map_err(|e| anyhow::anyhow!("invalid funding stream rate: {}", e))?;
                if rate_bps > 10_000 {
                    anyhow::bail!("funding stream rate exceeds 100% (10,000bps)");
                }
                Ok(FundingStream::ToAddress { address, rate_bps })
            }
            pb::funding_stream::Recipient::ToCommunityPool(to_community_pool) => {
                let rate_bps = to_community_pool
                    .rate_bps
                    .try_into()
                    .map_err(|e| anyhow::anyhow!("invalid funding stream rate: {}", e))?;
                if rate_bps > 10_000 {
                    anyhow::bail!("funding stream rate exceeds 100% (10,000bps)");
                }
                Ok(FundingStream::ToCommunityPool { rate_bps })
            }
        }
    }
}

/// A list of funding streams whose total commission is less than 100%.
///
/// The total commission of a validator is the sum of the individual reward rate of the
/// [`FundingStream`]s, and cannot exceed 10000bps (100%). This property is guaranteed by the
/// `TryFrom<Vec<FundingStream>` implementation for [`FundingStreams`], which checks the sum, and is
/// the only way to build a non-empty [`FundingStreams`].
///
/// Similarly, it's not possible to build a [`FundingStreams`] with more than 8 funding streams.
#[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct FundingStreams {
    funding_streams: Vec<FundingStream>,
}

impl FundingStreams {
    pub fn new() -> Self {
        Self {
            funding_streams: Vec::new(),
        }
    }

    pub fn iter(&self) -> impl Iterator<Item = &FundingStream> {
        self.funding_streams.iter()
    }

    pub fn len(&self) -> usize {
        self.funding_streams.len()
    }
}

impl TryFrom<Vec<FundingStream>> for FundingStreams {
    type Error = anyhow::Error;

    fn try_from(funding_streams: Vec<FundingStream>) -> Result<Self, Self::Error> {
        if funding_streams.len() > 8 {
            anyhow::bail!("validators can declare at most 8 funding streams");
        }

        if funding_streams.iter().map(|fs| fs.rate_bps()).sum::<u16>() > 10_000 {
            anyhow::bail!("sum of funding rates exceeds 100% (10,000bps)");
        }

        Ok(Self { funding_streams })
    }
}

impl From<FundingStreams> for Vec<FundingStream> {
    fn from(funding_streams: FundingStreams) -> Self {
        funding_streams.funding_streams
    }
}

impl AsRef<[FundingStream]> for FundingStreams {
    fn as_ref(&self) -> &[FundingStream] {
        &self.funding_streams
    }
}

impl IntoIterator for FundingStreams {
    type Item = FundingStream;
    type IntoIter = std::vec::IntoIter<FundingStream>;

    fn into_iter(self) -> Self::IntoIter {
        self.funding_streams.into_iter()
    }
}

impl<'a> IntoIterator for &'a FundingStreams {
    type Item = &'a FundingStream;
    type IntoIter = std::slice::Iter<'a, FundingStream>;

    fn into_iter(self) -> Self::IntoIter {
        (self.funding_streams).iter()
    }
}