Skip to content

Commit 848bd44

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

File tree

3 files changed

+355
-3
lines changed

3 files changed

+355
-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: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
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+
/// Computes bounds on inflation, token precision, and nominal proportional gain
16+
/// sufficient to yield non-zero rewards
17+
pub fn main() -> std::io::Result<()> {
18+
let stdin = std::io::stdin();
19+
20+
// Let Y be the total MASP epochs per year
21+
print!("MASP epochs per year ({}): ", DEFAULT_MASP_EPOCHS_PER_YEAR);
22+
std::io::stdout().flush()?;
23+
let mut masp_epochs_per_year = String::new();
24+
stdin.read_line(&mut masp_epochs_per_year)?;
25+
let masp_epochs_per_year = masp_epochs_per_year.trim();
26+
let masp_epochs_per_year = if masp_epochs_per_year.is_empty() {
27+
DEFAULT_MASP_EPOCHS_PER_YEAR
28+
} else {
29+
masp_epochs_per_year
30+
.parse::<u64>()
31+
.map_err(std::io::Error::other)?
32+
};
33+
34+
// Get the currency code for the native token
35+
print!("Native token currency code ({}): ", DEFAULT_NATIVE_CODE);
36+
std::io::stdout().flush()?;
37+
let mut native_code = String::new();
38+
stdin.read_line(&mut native_code)?;
39+
let native_code = native_code.trim();
40+
let native_code = if native_code.is_empty() {
41+
DEFAULT_NATIVE_CODE
42+
} else {
43+
native_code
44+
};
45+
46+
// Get the decimal places for the native token
47+
print!(
48+
"Native token decimal places ({}): ",
49+
DEFAULT_NATIVE_DECIMALS
50+
);
51+
std::io::stdout().flush()?;
52+
let mut native_decimals = String::new();
53+
stdin.read_line(&mut native_decimals)?;
54+
let native_decimals = native_decimals.trim();
55+
let native_decimals = if native_decimals.is_empty() {
56+
DEFAULT_NATIVE_DECIMALS
57+
} else {
58+
native_decimals
59+
.parse::<u8>()
60+
.map_err(std::io::Error::other)?
61+
};
62+
63+
// Let S be the total supply of NAM
64+
print!("Native token supply in {}: ", native_code);
65+
std::io::stdout().flush()?;
66+
let mut native_supply = String::new();
67+
stdin.read_line(&mut native_supply)?;
68+
let native_supply = native_supply
69+
.trim()
70+
.parse::<DenominatedAmount>()
71+
.map_err(std::io::Error::other)?;
72+
let native_supply = native_supply
73+
.increase_precision(native_decimals.into())
74+
.map_err(std::io::Error::other)?;
75+
76+
// Get the currency code for the incentivised token
77+
print!(
78+
"Incentivised token currency code ({}): ",
79+
DEFAULT_INCENT_CODE
80+
);
81+
std::io::stdout().flush()?;
82+
let mut incent_code = String::new();
83+
stdin.read_line(&mut incent_code)?;
84+
let incent_code = incent_code.trim();
85+
let incent_code = if incent_code.is_empty() {
86+
DEFAULT_INCENT_CODE
87+
} else {
88+
incent_code
89+
};
90+
91+
// Get the decimal places of the incentivised token
92+
print!(
93+
"Incentivised token decimal places ({}): ",
94+
DEFAULT_INCENT_DECIMALS
95+
);
96+
std::io::stdout().flush()?;
97+
let mut incent_decimals = String::new();
98+
stdin.read_line(&mut incent_decimals)?;
99+
let incent_decimals = incent_decimals.trim();
100+
let incent_decimals = if incent_decimals.is_empty() {
101+
DEFAULT_INCENT_DECIMALS
102+
} else {
103+
incent_decimals
104+
.parse::<u8>()
105+
.map_err(std::io::Error::other)?
106+
};
107+
108+
// Let X be the target amount of TOK locked in the MASP
109+
print!("Target locked amount in {}: ", incent_code);
110+
std::io::stdout().flush()?;
111+
let mut lock_target = String::new();
112+
stdin.read_line(&mut lock_target)?;
113+
let lock_target = lock_target
114+
.trim()
115+
.parse::<DenominatedAmount>()
116+
.map_err(std::io::Error::other)?;
117+
let lock_target = lock_target
118+
.increase_precision(incent_decimals.into())
119+
.map_err(std::io::Error::other)?;
120+
121+
// Let M be the desired minimum amount of TOK required to get rewards
122+
print!("Incentivisation threshold in {}: ", incent_code);
123+
std::io::stdout().flush()?;
124+
let mut incent_threshold = String::new();
125+
stdin.read_line(&mut incent_threshold)?;
126+
let incent_threshold = incent_threshold
127+
.trim()
128+
.parse::<DenominatedAmount>()
129+
.map_err(std::io::Error::other)?;
130+
let incent_threshold = incent_threshold
131+
.increase_precision(incent_decimals.into())
132+
.map_err(std::io::Error::other)?;
133+
134+
// It must be the case that X/I <= M. Or equivalently I >= X/M.
135+
let min_inflation = lock_target.amount().raw_amount()
136+
/ incent_threshold.amount().raw_amount();
137+
let min_inflation =
138+
DenominatedAmount::new(min_inflation.into(), native_decimals.into());
139+
println!(
140+
"Inflation must be more than {} {} to realize non-zero rewards for \
141+
users holding more than {} {} in a pool holding {} {}.",
142+
min_inflation,
143+
native_code,
144+
incent_threshold,
145+
incent_code,
146+
lock_target,
147+
incent_code
148+
);
149+
print!("Inflation (>= {}) in {}: ", min_inflation, native_code);
150+
std::io::stdout().flush()?;
151+
152+
// Let I be the computed amount of uNAM inflation in a single round of the
153+
// mechanism due to the incentivised token TOK.
154+
let mut inflation = String::new();
155+
stdin.read_line(&mut inflation)?;
156+
let inflation = inflation
157+
.trim()
158+
.parse::<DenominatedAmount>()
159+
.map_err(std::io::Error::other)?;
160+
let inflation = inflation
161+
.increase_precision(native_decimals.into())
162+
.map_err(std::io::Error::other)?;
163+
164+
// A necessary condition for there to be inflation is that floor(I*P/X)>=1.
165+
let min_precision =
166+
lock_target.amount().raw_amount() / inflation.amount().raw_amount();
167+
// A necessary condition for users holding more than the threshold to get
168+
// rewards is that P<=M.
169+
let max_precision = incent_threshold.amount().raw_amount();
170+
println!(
171+
"At an inflation of {} {}, the precision must be between {} and {} in \
172+
order to realize non-zero rewards.",
173+
inflation, native_code, min_precision, max_precision
174+
);
175+
176+
// Let P be the precision of the token TOK
177+
print!("Precision ([{}, {}]): ", min_precision, max_precision);
178+
std::io::stdout().flush()?;
179+
let mut precision = String::new();
180+
stdin.read_line(&mut precision)?;
181+
let precision = precision
182+
.trim()
183+
.parse::<Dec>()
184+
.map_err(std::io::Error::other)?;
185+
println!(
186+
"Users holding at least {} {} in the shielded pool will receive \
187+
rewards.",
188+
precision, incent_code
189+
);
190+
191+
// It must be that S*C/Y >= I, which implies that the reward rate
192+
// C >= (I*Y)/S
193+
let inflation_per_year = inflation
194+
.checked_mul(DenominatedAmount::new(
195+
masp_epochs_per_year.into(),
196+
0.into(),
197+
))
198+
.ok_or(std::io::Error::other(
199+
"unable to compute inflation per year",
200+
))?;
201+
let max_reward_rate_threshold = inflation_per_year
202+
.checked_div_precision(
203+
native_supply,
204+
MAX_REWARD_RATE_THRESHOLD_DECIMALS.into(),
205+
)
206+
.ok_or(std::io::Error::other(
207+
"unable to divide inflation by native supply",
208+
))?;
209+
println!(
210+
"The maximum reward rate must exceed {} for it to be possible to \
211+
achieve an inflation of {} {}.",
212+
max_reward_rate_threshold, inflation, native_code
213+
);
214+
215+
// Let C be the maximum reward rate for the token TOK
216+
print!("Maximum reward rate (>= {}): ", max_reward_rate_threshold);
217+
std::io::stdout().flush()?;
218+
let mut maximum_reward_rate = String::new();
219+
stdin.read_line(&mut maximum_reward_rate)?;
220+
let maximum_reward_rate = maximum_reward_rate
221+
.trim()
222+
.parse::<DenominatedAmount>()
223+
.map_err(std::io::Error::other)?;
224+
225+
// Let T be the threshold such that shielded rewards are guaranteed when E
226+
// exceeds T
227+
print!(
228+
"Locked amount tolerance as proportion of target ({}): ",
229+
DEFAULT_INFLATION_THRESHOLD
230+
);
231+
std::io::stdout().flush()?;
232+
let mut inflation_threshold = String::new();
233+
stdin.read_line(&mut inflation_threshold)?;
234+
let inflation_threshold = inflation_threshold.trim();
235+
let inflation_threshold = if inflation_threshold.is_empty() {
236+
DEFAULT_INFLATION_THRESHOLD
237+
.parse::<DenominatedAmount>()
238+
.map_err(std::io::Error::other)?
239+
} else {
240+
inflation_threshold
241+
.parse::<DenominatedAmount>()
242+
.map_err(std::io::Error::other)?
243+
};
244+
let inflation_threshold = lock_target
245+
.checked_mul(inflation_threshold)
246+
.ok_or(std::io::Error::other(
247+
"unable to multiply lock target by inflation threshold",
248+
))?
249+
.approximate(native_decimals.into())
250+
.map_err(std::io::Error::other)?
251+
.0;
252+
253+
// Setting KP_nom >= (I*Y)/(C*T) makes the control value exceed I since it
254+
// implies KP_nom*(C/Y)*T >= I which implies KP_nom*(C/Y)*E >= I
255+
let nominal_proportional_gain_threshold = inflation_per_year
256+
.redenominate(0)
257+
.checked_div_precision(
258+
maximum_reward_rate
259+
.checked_mul(inflation_threshold.redenominate(0))
260+
.ok_or(std::io::Error::other(
261+
"unable to multiply maximum reward rate by inflation \
262+
threshold",
263+
))?,
264+
KP_GAIN_DECIMALS.into(),
265+
)
266+
.ok_or(std::io::Error::other(
267+
"unable to multiply maximum reward rate by inflation threshold",
268+
))?;
269+
println!(
270+
"Under the assumption that the locked amount is more than {} {} less \
271+
than the target and the error derivative is non-positive, a nominal \
272+
proportional gain exceeding {} is sufficient to achieve inflation \
273+
exceeding {} {}",
274+
inflation_threshold,
275+
incent_code,
276+
nominal_proportional_gain_threshold,
277+
inflation,
278+
native_code
279+
);
280+
Ok(())
281+
}

0 commit comments

Comments
 (0)