Skip to content

Commit 471e5fd

Browse files
committed
Added an example program to derive constraints on shielded reward parameters.
1 parent 4573d99 commit 471e5fd

File tree

3 files changed

+328
-3
lines changed

3 files changed

+328
-3
lines changed

crates/core/src/token.rs

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -512,8 +512,8 @@ impl DenominatedAmount {
512512
}
513513
}
514514

515-
/// Attempt to increase the precision of an amount. Can fail
516-
/// if the resulting amount does not fit into 256 bits.
515+
/// Return an equivalent denominated amount with the given denomination. Can
516+
/// fail if the resulting amount does not fit into 256 bits.
517517
pub fn increase_precision(
518518
self,
519519
denom: Denomination,
@@ -534,8 +534,43 @@ impl DenominatedAmount {
534534
.ok_or(AmountParseError::PrecisionOverflow)
535535
}
536536

537+
/// Return the closest denominated amount with the given denomination and
538+
/// the error.
539+
pub fn approximate(
540+
self,
541+
denom: Denomination,
542+
) -> Result<(Self, Self), AmountParseError> {
543+
if denom.0 < self.denom.0 {
544+
// Divide numerator and denominator by a power of 10
545+
let amount = self.amount.raw_amount();
546+
#[allow(clippy::arithmetic_side_effects)]
547+
let (quot, rem) = Uint::from(10)
548+
.checked_pow(Uint::from(self.denom.0 - denom.0))
549+
.and_then(|scaling| {
550+
amount.checked_div(scaling).zip(amount.checked_rem(scaling))
551+
})
552+
.ok_or(AmountParseError::PrecisionOverflow)?;
553+
let approx = Self {
554+
amount: quot.into(),
555+
denom,
556+
};
557+
let error = Self {
558+
amount: rem.into(),
559+
denom: self.denom,
560+
};
561+
Ok((approx, error))
562+
} else {
563+
// Multiply numerator and denominator by a power of 10
564+
let error = Self {
565+
amount: 0.into(),
566+
denom: self.denom,
567+
};
568+
self.increase_precision(denom).map(|x| (x, error))
569+
}
570+
}
571+
537572
/// Create a new [`DenominatedAmount`] with the same underlying
538-
/// amout but a new denomination.
573+
/// amount but a new denomination.
539574
pub fn redenominate(self, new_denom: u8) -> Self {
540575
Self {
541576
amount: self.amount,
@@ -590,6 +625,38 @@ impl DenominatedAmount {
590625
})
591626
}
592627

628+
/// Checked division computed to the given precission. Returns `None` on
629+
/// overflow.
630+
pub fn checked_div_precision(
631+
&self,
632+
rhs: DenominatedAmount,
633+
denom: Denomination,
634+
) -> Option<Self> {
635+
#[allow(clippy::arithmetic_side_effects)]
636+
let pow = i16::from(rhs.denom.0) + i16::from(denom.0)
637+
- i16::from(self.denom.0);
638+
if pow < 0 {
639+
return None;
640+
}
641+
let amount = Uint::from(10).checked_pow(Uint::from(pow)).and_then(
642+
|scaling| {
643+
scaling.checked_mul_div(
644+
self.amount.raw_amount(),
645+
rhs.amount.raw_amount(),
646+
)
647+
},
648+
)?;
649+
Some(Self {
650+
amount: amount.0.into(),
651+
denom,
652+
})
653+
}
654+
655+
/// Checked division. Returns `None` on overflow.
656+
pub fn checked_div(&self, rhs: DenominatedAmount) -> Option<Self> {
657+
self.checked_div_precision(rhs, self.denom)
658+
}
659+
593660
/// Returns the significand of this number
594661
pub const fn amount(&self) -> Amount {
595662
self.amount

examples/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ version.workspace = true
1616
name = "generate-txs"
1717
path = "generate_txs.rs"
1818

19+
[[example]]
20+
name = "shielded-rewards"
21+
path = "shielded_rewards.rs"
22+
1923
[[example]]
2024
name = "tx-schema"
2125
path = "tx_schema.rs"

examples/shielded_rewards.rs

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
use std::io::Write;
2+
3+
use namada_core::dec::Dec;
4+
use namada_core::token::DenominatedAmount;
5+
6+
const DEFAULT_NATIVE_CODE: &str = "NAM";
7+
const DEFAULT_NATIVE_DECIMALS: u8 = 6;
8+
const DEFAULT_INCENT_CODE: &str = "OSMO";
9+
const DEFAULT_INCENT_DECIMALS: u8 = 6;
10+
const DEFAULT_MASP_EPOCHS_PER_YEAR: u64 = 365;
11+
const DEFAULT_INFLATION_THRESHOLD: &str = "0.01";
12+
const KP_GAIN_DECIMALS: u8 = 6;
13+
const MAX_REWARD_RATE_THRESHOLD_DECIMALS: u8 = 75;
14+
15+
fn main() -> std::io::Result<()> {
16+
let stdin = std::io::stdin();
17+
18+
print!("MASP epochs per year ({}): ", DEFAULT_MASP_EPOCHS_PER_YEAR);
19+
std::io::stdout().flush()?;
20+
let mut masp_epochs_per_year = String::new();
21+
stdin.read_line(&mut masp_epochs_per_year)?;
22+
let masp_epochs_per_year = masp_epochs_per_year.trim();
23+
let masp_epochs_per_year = if masp_epochs_per_year.is_empty() {
24+
DEFAULT_MASP_EPOCHS_PER_YEAR
25+
} else {
26+
masp_epochs_per_year
27+
.parse::<u64>()
28+
.map_err(std::io::Error::other)?
29+
};
30+
31+
print!("Native token currency code ({}): ", DEFAULT_NATIVE_CODE);
32+
std::io::stdout().flush()?;
33+
let mut native_code = String::new();
34+
stdin.read_line(&mut native_code)?;
35+
let native_code = native_code.trim();
36+
let native_code = if native_code.is_empty() {
37+
DEFAULT_NATIVE_CODE
38+
} else {
39+
native_code
40+
};
41+
42+
print!(
43+
"Native token decimal places ({}): ",
44+
DEFAULT_NATIVE_DECIMALS
45+
);
46+
std::io::stdout().flush()?;
47+
let mut native_decimals = String::new();
48+
stdin.read_line(&mut native_decimals)?;
49+
let native_decimals = native_decimals.trim();
50+
let native_decimals = if native_decimals.is_empty() {
51+
DEFAULT_NATIVE_DECIMALS
52+
} else {
53+
native_decimals
54+
.parse::<u8>()
55+
.map_err(std::io::Error::other)?
56+
};
57+
58+
print!("Native token supply in {}: ", native_code);
59+
std::io::stdout().flush()?;
60+
let mut native_supply = String::new();
61+
stdin.read_line(&mut native_supply)?;
62+
let native_supply = native_supply
63+
.trim()
64+
.parse::<DenominatedAmount>()
65+
.map_err(std::io::Error::other)?;
66+
let native_supply = native_supply
67+
.increase_precision(native_decimals.into())
68+
.map_err(std::io::Error::other)?;
69+
70+
print!(
71+
"Incentivised token currency code ({}): ",
72+
DEFAULT_INCENT_CODE
73+
);
74+
std::io::stdout().flush()?;
75+
let mut incent_code = String::new();
76+
stdin.read_line(&mut incent_code)?;
77+
let incent_code = incent_code.trim();
78+
let incent_code = if incent_code.is_empty() {
79+
DEFAULT_INCENT_CODE
80+
} else {
81+
incent_code
82+
};
83+
84+
print!(
85+
"Incentivised token decimal places ({}): ",
86+
DEFAULT_INCENT_DECIMALS
87+
);
88+
std::io::stdout().flush()?;
89+
let mut incent_decimals = String::new();
90+
stdin.read_line(&mut incent_decimals)?;
91+
let incent_decimals = incent_decimals.trim();
92+
let incent_decimals = if incent_decimals.is_empty() {
93+
DEFAULT_INCENT_DECIMALS
94+
} else {
95+
incent_decimals
96+
.parse::<u8>()
97+
.map_err(std::io::Error::other)?
98+
};
99+
100+
print!("Target locked amount in {}: ", incent_code);
101+
std::io::stdout().flush()?;
102+
let mut lock_target = String::new();
103+
stdin.read_line(&mut lock_target)?;
104+
let lock_target = lock_target
105+
.trim()
106+
.parse::<DenominatedAmount>()
107+
.map_err(std::io::Error::other)?;
108+
let lock_target = lock_target
109+
.increase_precision(incent_decimals.into())
110+
.map_err(std::io::Error::other)?;
111+
112+
print!("Incentivisation threshold in {}: ", incent_code);
113+
std::io::stdout().flush()?;
114+
let mut incent_threshold = String::new();
115+
stdin.read_line(&mut incent_threshold)?;
116+
let incent_threshold = incent_threshold
117+
.trim()
118+
.parse::<DenominatedAmount>()
119+
.map_err(std::io::Error::other)?;
120+
let incent_threshold = incent_threshold
121+
.increase_precision(incent_decimals.into())
122+
.map_err(std::io::Error::other)?;
123+
124+
let min_inflation = lock_target.amount().raw_amount()
125+
/ incent_threshold.amount().raw_amount();
126+
let min_inflation =
127+
DenominatedAmount::new(min_inflation.into(), native_decimals.into());
128+
println!(
129+
"Inflation must be more than {} {} to realize non-zero rewards for \
130+
users holding more than {} {} in a pool holding {} {}.",
131+
min_inflation,
132+
native_code,
133+
incent_threshold,
134+
incent_code,
135+
lock_target,
136+
incent_code
137+
);
138+
print!("Inflation (>= {}) in {}: ", min_inflation, native_code);
139+
std::io::stdout().flush()?;
140+
let mut inflation = String::new();
141+
stdin.read_line(&mut inflation)?;
142+
let inflation = inflation
143+
.trim()
144+
.parse::<DenominatedAmount>()
145+
.map_err(std::io::Error::other)?;
146+
let inflation = inflation
147+
.increase_precision(native_decimals.into())
148+
.map_err(std::io::Error::other)?;
149+
150+
let min_precision =
151+
lock_target.amount().raw_amount() / inflation.amount().raw_amount();
152+
let max_precision = incent_threshold.amount().raw_amount();
153+
println!(
154+
"At an inflation of {} {}, the precision must be between {} and {} in \
155+
order to realize non-zero rewards.",
156+
inflation, native_code, min_precision, max_precision
157+
);
158+
print!("Precision ([{}, {}]): ", min_precision, max_precision);
159+
std::io::stdout().flush()?;
160+
let mut precision = String::new();
161+
stdin.read_line(&mut precision)?;
162+
let precision = precision
163+
.trim()
164+
.parse::<Dec>()
165+
.map_err(std::io::Error::other)?;
166+
println!(
167+
"Users holding at least {} {} in the shielded pool will receive \
168+
rewards.",
169+
precision, incent_code
170+
);
171+
172+
let inflation_per_year = inflation
173+
.checked_mul(DenominatedAmount::new(
174+
masp_epochs_per_year.into(),
175+
0.into(),
176+
))
177+
.ok_or(std::io::Error::other(
178+
"unable to compute inflation per year",
179+
))?;
180+
let max_reward_rate_threshold = inflation_per_year
181+
.checked_div_precision(
182+
native_supply,
183+
MAX_REWARD_RATE_THRESHOLD_DECIMALS.into(),
184+
)
185+
.ok_or(std::io::Error::other(
186+
"unable to divide inflation by native supply",
187+
))?;
188+
println!(
189+
"The maximum reward rate must exceed {} for it to be possible to \
190+
achieve an inflation of {} {}.",
191+
max_reward_rate_threshold, inflation, native_code
192+
);
193+
print!("Maximum reward rate (>= {}): ", max_reward_rate_threshold);
194+
std::io::stdout().flush()?;
195+
let mut maximum_reward_rate = String::new();
196+
stdin.read_line(&mut maximum_reward_rate)?;
197+
let maximum_reward_rate = maximum_reward_rate
198+
.trim()
199+
.parse::<DenominatedAmount>()
200+
.map_err(std::io::Error::other)?;
201+
202+
print!(
203+
"Locked amount tolerance as proportion of target ({}): ",
204+
DEFAULT_INFLATION_THRESHOLD
205+
);
206+
std::io::stdout().flush()?;
207+
let mut inflation_threshold = String::new();
208+
stdin.read_line(&mut inflation_threshold)?;
209+
let inflation_threshold = inflation_threshold.trim();
210+
let inflation_threshold = if inflation_threshold.is_empty() {
211+
DEFAULT_INFLATION_THRESHOLD
212+
.parse::<DenominatedAmount>()
213+
.map_err(std::io::Error::other)?
214+
} else {
215+
inflation_threshold
216+
.parse::<DenominatedAmount>()
217+
.map_err(std::io::Error::other)?
218+
};
219+
220+
let inflation_threshold = lock_target
221+
.checked_mul(inflation_threshold)
222+
.ok_or(std::io::Error::other(
223+
"unable to multiply lock target by inflation threshold",
224+
))?
225+
.approximate(native_decimals.into())
226+
.map_err(std::io::Error::other)?
227+
.0;
228+
let nominal_proportional_gain_threshold = inflation_per_year
229+
.redenominate(0)
230+
.checked_div_precision(
231+
maximum_reward_rate
232+
.checked_mul(inflation_threshold.redenominate(0))
233+
.ok_or(std::io::Error::other(
234+
"unable to multiply maximum reward rate by inflation \
235+
threshold",
236+
))?,
237+
KP_GAIN_DECIMALS.into(),
238+
)
239+
.ok_or(std::io::Error::other(
240+
"unable to multiply maximum reward rate by inflation threshold",
241+
))?;
242+
println!(
243+
"Under the assumption that the locked amount is more than {} {} less \
244+
than the target and the error derivative is non-positive, a nominal \
245+
proportional gain exceeding {} is sufficient to achieve inflation \
246+
exceeding {} {}",
247+
inflation_threshold,
248+
incent_code,
249+
nominal_proportional_gain_threshold,
250+
inflation,
251+
native_code
252+
);
253+
Ok(())
254+
}

0 commit comments

Comments
 (0)