-
Notifications
You must be signed in to change notification settings - Fork 298
/
lib.rs
168 lines (142 loc) · 6.29 KB
/
lib.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
use ic_sns_governance_token_valuation::{Valuation, ValuationFactors};
use num_traits::ops::inv::Inv;
use rust_decimal::Decimal;
use rust_decimal_macros::dec;
const ONE_QUARTER: Decimal = dec!(0.25);
pub fn transfer_sns_treasury_funds_7_day_total_upper_bound_tokens(
valuation: Valuation,
) -> Result<Decimal, ProposalsAmountTotalLimitError> {
ProposalsAmountTotalUpperBound::in_tokens(valuation)
}
pub fn mint_sns_tokens_7_day_total_upper_bound_tokens(
valuation: Valuation,
) -> Result<Decimal, ProposalsAmountTotalLimitError> {
ProposalsAmountTotalUpperBound::in_tokens(valuation)
}
/// Within a 7 day window, at most, this much of the treasury can be transferred (out via proposal).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProposalsAmountTotalUpperBound {
/// Any amount can be transferred (out via proposal).
NoLimit,
/// How much of the treasury can be transferred (out via proposal). A real number in the real
/// number interval [0.0, 1.0], where 0 = 0% and 1.0 = 100%.
Fraction(Decimal),
/// How much of the treasury can be transferred (out via proposal) stated in XDR.
Xdr(Decimal),
}
impl ProposalsAmountTotalUpperBound {
// A treasury can be small, medium, or large. These are the boundaries between those regimes.
const MAX_SMALL_TREASURY_SIZE_XDR: Decimal = dec!(100_000);
const MAX_MEDIUM_TREASURY_SIZE_XDR: Decimal = dec!(1_200_000);
// No matter how large the treasury is, not more than this amount can be removed (within a 7 day
// window).
const MAX_XDR: Decimal = dec!(300_000);
/// A price quote less than this is considered "unrealistically" low. When that happens, we use
/// this instead of the quoted value.
///
/// # Motivation
///
/// Low XDRs per ICP quotes would tend to cause our valuations to be in the "small" regime,
/// where an SNS is allowed to take the biggest actions relative to their size. This is to
/// minmize the damage caused by wacky price quotes.
///
/// # What Value to Use
///
/// Currently, the minimum XDRs per ICP used by NNS governance is 1. This is simply copied from
/// there, specifically from the minimum_icp_xdr_rate field in NetworkEconomics.
///
/// As of Mar 2024, the price of ICP is around 10 XDR. The lowest it has ever been is around 2.2
/// XDR. FWIW, this is less than that.
///
/// # Why Not Also Define MAX?
///
/// Currently, we do not have/enforce a MAX_XDRS_PER_ICP, because this would tend to cause our
/// valuations to be in the "large" regime, where actions are more limited.
const MIN_XDRS_PER_ICP: Decimal = dec!(1);
fn in_tokens(mut valuation: Valuation) -> Result<Decimal, ProposalsAmountTotalLimitError> {
Self::clamp_xdrs_per_icp(&mut valuation);
let ValuationFactors {
tokens: balance_tokens,
icps_per_token,
xdrs_per_icp,
} = valuation.valuation_factors;
let self_ = Self::from_valuation_xdr(valuation.to_xdr());
let result_tokens = match self_ {
Self::NoLimit => balance_tokens,
Self::Fraction(fraction) => balance_tokens
.checked_mul(fraction)
// Overflow should not be possible, since fraction is supposed to be at most 1.0.
.ok_or_else(|| {
ProposalsAmountTotalLimitError::new_arithmetic(format!(
"Unable to perform {} * {}.",
balance_tokens, fraction,
))
})?,
Self::Xdr(max_xdr) => {
let xdrs_per_token = xdrs_per_icp.checked_mul(icps_per_token).ok_or_else(|| {
ProposalsAmountTotalLimitError::new_arithmetic(format!(
"XDRs per token could not be calculated from valuation: {:?}",
valuation
))
})?;
// Calculate the inverse conversion rate.
if xdrs_per_token == Decimal::from(0) {
// This is not reachable, because in this case, valuation.to_xdr() would return
// 0, and in that case, we would have taken the NoLimit branch.
return Err(ProposalsAmountTotalLimitError::new_arithmetic(format!(
"It appears that the tokens have zero value in XDR. valuation = {:?}",
valuation
)));
}
let tokens_per_xdr = xdrs_per_token.inv();
max_xdr.checked_mul(tokens_per_xdr).ok_or_else(|| {
ProposalsAmountTotalLimitError::new_arithmetic(format!(
"Max tokens could not be calculated with valuation: {:?}",
valuation,
))
})?
}
};
Ok(result_tokens)
}
fn from_valuation_xdr(valuation_xdr: Decimal) -> Self {
// Ideally, this would be checked at compile time. In principal, this should be possible,
// since all the inputs are const, but I'm not sure how to do that. Therefore,
// debug_assert_eq is used instead, and should be very nearly as good, because this will be
// run during CI.
debug_assert_eq!(
Self::MAX_MEDIUM_TREASURY_SIZE_XDR.checked_mul(ONE_QUARTER),
Some(Self::MAX_XDR),
);
if valuation_xdr <= Self::MAX_SMALL_TREASURY_SIZE_XDR {
return Self::NoLimit;
}
if valuation_xdr <= Self::MAX_MEDIUM_TREASURY_SIZE_XDR {
return Self::Fraction(ONE_QUARTER);
}
Self::Xdr(Self::MAX_XDR)
}
fn clamp_xdrs_per_icp(valuation: &mut Valuation) {
let xdrs_per_icp = &mut valuation.valuation_factors.xdrs_per_icp;
*xdrs_per_icp = (*xdrs_per_icp).max(Self::MIN_XDRS_PER_ICP);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProposalsAmountTotalLimitError {
pub error_type: ProposalsAmountTotalLimitErrorType,
pub message: String,
}
impl ProposalsAmountTotalLimitError {
pub fn new_arithmetic(message: String) -> Self {
Self {
error_type: ProposalsAmountTotalLimitErrorType::Arithmetic,
message,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProposalsAmountTotalLimitErrorType {
Arithmetic,
}
#[cfg(test)]
mod tests;