Skip to content

Commit e603759

Browse files
authored
Merge pull request #3816 from anoma/brent/estimate-staking-reward-rate
Estimate annual staking rewards rate
2 parents f7830f6 + 8c4e722 commit e603759

File tree

9 files changed

+239
-3
lines changed

9 files changed

+239
-3
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
- Adds an SDK and CLI tool to estimate the latest annual staking rewards rate.
2+
([\#3816](https://github.com/anoma/namada/pull/3816))

crates/apps_lib/src/cli.rs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ pub mod cmds {
298298
.subcommand(QueryMetaData::def().display_order(5))
299299
.subcommand(QueryTotalSupply::def().display_order(5))
300300
.subcommand(QueryEffNativeSupply::def().display_order(5))
301+
.subcommand(QueryStakingRewardsRate::def().display_order(5))
301302
// Actions
302303
.subcommand(SignTx::def().display_order(6))
303304
.subcommand(ShieldedSync::def().display_order(6))
@@ -373,6 +374,8 @@ pub mod cmds {
373374
Self::parse_with_ctx(matches, QueryTotalSupply);
374375
let query_native_supply =
375376
Self::parse_with_ctx(matches, QueryEffNativeSupply);
377+
let query_staking_rewards_rate =
378+
Self::parse_with_ctx(matches, QueryStakingRewardsRate);
376379
let query_find_validator =
377380
Self::parse_with_ctx(matches, QueryFindValidator);
378381
let query_result = Self::parse_with_ctx(matches, QueryResult);
@@ -449,6 +452,7 @@ pub mod cmds {
449452
.or(query_metadata)
450453
.or(query_total_supply)
451454
.or(query_native_supply)
455+
.or(query_staking_rewards_rate)
452456
.or(query_account)
453457
.or(sign_tx)
454458
.or(shielded_sync)
@@ -534,6 +538,7 @@ pub mod cmds {
534538
QueryDelegations(QueryDelegations),
535539
QueryTotalSupply(QueryTotalSupply),
536540
QueryEffNativeSupply(QueryEffNativeSupply),
541+
QueryStakingRewardsRate(QueryStakingRewardsRate),
537542
QueryFindValidator(QueryFindValidator),
538543
QueryRawBytes(QueryRawBytes),
539544
QueryProposal(QueryProposal),
@@ -2118,6 +2123,36 @@ pub mod cmds {
21182123
}
21192124
}
21202125

2126+
#[derive(Clone, Debug)]
2127+
pub struct QueryStakingRewardsRate(
2128+
pub args::QueryStakingRewardsRate<args::CliTypes>,
2129+
);
2130+
2131+
impl SubCmd for QueryStakingRewardsRate {
2132+
const CMD: &'static str = "staking-rewards-rate";
2133+
2134+
fn parse(matches: &ArgMatches) -> Option<Self>
2135+
where
2136+
Self: Sized,
2137+
{
2138+
matches.subcommand_matches(Self::CMD).map(|matches| {
2139+
QueryStakingRewardsRate(args::QueryStakingRewardsRate::parse(
2140+
matches,
2141+
))
2142+
})
2143+
}
2144+
2145+
fn def() -> App {
2146+
App::new(Self::CMD)
2147+
.about(wrap!(
2148+
"Query the latest estimate of the staking rewards rate \
2149+
based on the most recent minted inflation amount at the \
2150+
last epoch change."
2151+
))
2152+
.add_args::<args::QueryStakingRewardsRate<args::CliTypes>>()
2153+
}
2154+
}
2155+
21212156
#[derive(Clone, Debug)]
21222157
pub struct QueryFindValidator(pub args::QueryFindValidator<args::CliTypes>);
21232158

@@ -7157,6 +7192,32 @@ pub mod args {
71577192
}
71587193
}
71597194

7195+
impl Args for QueryStakingRewardsRate<CliTypes> {
7196+
fn parse(matches: &ArgMatches) -> Self {
7197+
let query = Query::parse(matches);
7198+
Self { query }
7199+
}
7200+
7201+
fn def(app: App) -> App {
7202+
app.add_args::<Query<CliTypes>>()
7203+
}
7204+
}
7205+
7206+
impl CliToSdk<QueryStakingRewardsRate<SdkTypes>>
7207+
for QueryStakingRewardsRate<CliTypes>
7208+
{
7209+
type Error = std::convert::Infallible;
7210+
7211+
fn to_sdk(
7212+
self,
7213+
ctx: &mut Context,
7214+
) -> Result<QueryStakingRewardsRate<SdkTypes>, Self::Error> {
7215+
Ok(QueryStakingRewardsRate::<SdkTypes> {
7216+
query: self.query.to_sdk(ctx)?,
7217+
})
7218+
}
7219+
}
7220+
71607221
impl Args for QueryFindValidator<CliTypes> {
71617222
fn parse(matches: &ArgMatches) -> Self {
71627223
let query = Query::parse(matches);

crates/apps_lib/src/cli/client.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -661,6 +661,19 @@ impl CliApi {
661661
let namada = ctx.to_sdk(client, io);
662662
rpc::query_effective_native_supply(&namada).await;
663663
}
664+
Sub::QueryStakingRewardsRate(QueryStakingRewardsRate(
665+
args,
666+
)) => {
667+
let chain_ctx = ctx.borrow_mut_chain_or_exit();
668+
let ledger_address =
669+
chain_ctx.get(&args.query.ledger_address);
670+
let client = client.unwrap_or_else(|| {
671+
C::from_tendermint_address(&ledger_address)
672+
});
673+
client.wait_until_node_is_synced(&io).await?;
674+
let namada = ctx.to_sdk(client, io);
675+
rpc::query_staking_rewards_rate(&namada).await;
676+
}
664677
Sub::QueryFindValidator(QueryFindValidator(args)) => {
665678
let chain_ctx = ctx.borrow_mut_chain_or_exit();
666679
let ledger_address =

crates/apps_lib/src/client/rpc.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use namada_sdk::address::{Address, InternalAddress, MASP};
1515
use namada_sdk::chain::{BlockHeight, Epoch};
1616
use namada_sdk::collections::{HashMap, HashSet};
1717
use namada_sdk::control_flow::time::{Duration, Instant};
18+
use namada_sdk::dec::Dec;
1819
use namada_sdk::events::Event;
1920
use namada_sdk::governance::parameters::GovernanceParameters;
2021
use namada_sdk::governance::pgf::parameters::PgfParameters;
@@ -1364,6 +1365,21 @@ pub async fn query_effective_native_supply<N: Namada>(context: &N) {
13641365
display_line!(context.io(), "nam: {}", native_supply.to_string_native());
13651366
}
13661367

1368+
/// Query the staking rewards rate estimate
1369+
pub async fn query_staking_rewards_rate<N: Namada>(context: &N) {
1370+
let rewards_rate = unwrap_client_response::<N::Client, Dec>(
1371+
RPC.vp()
1372+
.token()
1373+
.staking_rewards_rate(context.client())
1374+
.await,
1375+
);
1376+
display_line!(
1377+
context.io(),
1378+
"Current annual staking rewards rate: {}",
1379+
rewards_rate
1380+
);
1381+
}
1382+
13671383
/// Query a validator's state information
13681384
pub async fn query_and_print_validator_state(
13691385
context: &impl Namada,

crates/proof_of_stake/src/rewards.rs

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,11 +656,52 @@ where
656656
Ok(storage.read::<token::Amount>(&key)?.unwrap_or_default())
657657
}
658658

659+
/// Compute an estimation of the most recent staking rewards rate.
660+
pub fn estimate_staking_reward_rate<S, Token, Parameters>(
661+
storage: &S,
662+
) -> Result<Dec>
663+
where
664+
S: StorageRead,
665+
Parameters: parameters::Read<S>,
666+
Token: trans_token::Read<S> + trans_token::Write<S>,
667+
{
668+
// Get needed data in desired form
669+
let total_native_tokens =
670+
Token::get_effective_total_native_supply(storage)?;
671+
let last_staked_ratio = read_last_staked_ratio(storage)?
672+
.expect("Last staked ratio should exist in PoS storage");
673+
let last_inflation_amount = read_last_pos_inflation_amount(storage)?
674+
.expect("Last inflation amount should exist in PoS storage");
675+
let epochs_per_year: u64 = Parameters::epochs_per_year(storage)?;
676+
677+
let total_native_tokens =
678+
Dec::try_from(total_native_tokens).into_storage_result()?;
679+
let last_inflation_amount =
680+
Dec::try_from(last_inflation_amount).into_storage_result()?;
681+
682+
// Estimate annual inflation rate
683+
let est_inflation_rate = checked!(
684+
last_inflation_amount * epochs_per_year / total_native_tokens
685+
)?;
686+
687+
// Estimate annual staking rewards rate
688+
let est_staking_reward_rate =
689+
checked!(est_inflation_rate / last_staked_ratio)?;
690+
691+
Ok(est_staking_reward_rate)
692+
}
693+
659694
#[cfg(test)]
660695
mod tests {
661696
use std::str::FromStr;
662697

698+
use namada_parameters::storage::get_epochs_per_year_key;
699+
use namada_state::testing::TestState;
700+
use namada_trans_token::storage_key::minted_balance_key;
701+
use storage::write_pos_params;
702+
663703
use super::*;
704+
use crate::OwnedPosParams;
664705

665706
#[test]
666707
fn test_inflation_calc_up() {
@@ -842,10 +883,19 @@ mod tests {
842883

843884
#[test]
844885
fn test_pos_inflation_playground() {
886+
let mut storage = TestState::default();
887+
let gov_params =
888+
namada_governance::parameters::GovernanceParameters::default();
889+
gov_params.init_storage(&mut storage).unwrap();
890+
write_pos_params(&mut storage, &OwnedPosParams::default()).unwrap();
891+
845892
let epochs_per_year = 365_u64;
893+
let epy_key = get_epochs_per_year_key();
894+
storage.write(&epy_key, epochs_per_year).unwrap();
846895

847896
let init_locked_ratio = Dec::from_str("0.1").unwrap();
848897
let mut last_locked_ratio = init_locked_ratio;
898+
849899
let total_native_tokens = 1_000_000_000_u64;
850900
let locked_amount = u64::try_from(
851901
(init_locked_ratio * total_native_tokens).to_uint().unwrap(),
@@ -856,6 +906,13 @@ mod tests {
856906
let mut total_native_tokens =
857907
token::Amount::native_whole(total_native_tokens);
858908

909+
update_state_for_pos_playground(
910+
&mut storage,
911+
last_locked_ratio,
912+
last_inflation_amount,
913+
total_native_tokens,
914+
);
915+
859916
let max_reward_rate = Dec::from_str("0.1").unwrap();
860917
let target_ratio = Dec::from_str("0.66666666").unwrap();
861918
let p_gain_nom = Dec::from_str("0.25").unwrap();
@@ -882,17 +939,42 @@ mod tests {
882939
let locked_ratio = Dec::try_from(locked_amount).unwrap()
883940
/ Dec::try_from(total_native_tokens).unwrap();
884941

885-
let rate = Dec::try_from(inflation).unwrap()
942+
let inflation_rate = Dec::try_from(inflation).unwrap()
886943
* Dec::from(epochs_per_year)
887944
/ Dec::try_from(total_native_tokens).unwrap();
945+
let staking_rate = inflation_rate / locked_ratio;
946+
888947
println!(
889948
"Round {round}: Locked ratio: {locked_ratio}, inflation rate: \
890-
{rate}",
949+
{inflation_rate}, staking rate: {staking_rate}",
891950
);
892951

893952
last_inflation_amount = inflation;
894953
total_native_tokens += inflation;
895954
last_locked_ratio = locked_ratio;
955+
update_state_for_pos_playground(
956+
&mut storage,
957+
last_locked_ratio,
958+
last_inflation_amount,
959+
total_native_tokens,
960+
);
961+
962+
let query_staking_rate = estimate_staking_reward_rate::<
963+
_,
964+
namada_trans_token::Store<_>,
965+
namada_parameters::Store<_>,
966+
>(&storage)
967+
.unwrap();
968+
// println!(" ----> Query staking rate: {query_staking_rate}");
969+
if !staking_rate.is_zero() && !query_staking_rate.is_zero() {
970+
let ratio = staking_rate / query_staking_rate;
971+
let residual = ratio.abs_diff(Dec::one()).unwrap();
972+
assert!(residual < Dec::from_str("0.001").unwrap());
973+
// println!(
974+
// " ----> Ratio: {}\n",
975+
// staking_rate / query_staking_rate
976+
// );
977+
}
896978

897979
// if rate.abs_diff(&controller.max_reward_rate)
898980
// < Dec::from_str("0.01").unwrap()
@@ -930,4 +1012,22 @@ mod tests {
9301012
// );
9311013
}
9321014
}
1015+
1016+
fn update_state_for_pos_playground<S>(
1017+
storage: &mut S,
1018+
last_staked_ratio: Dec,
1019+
last_inflation_amount: token::Amount,
1020+
total_native_amount: token::Amount,
1021+
) where
1022+
S: StorageRead + StorageWrite,
1023+
{
1024+
write_last_staked_ratio(storage, last_staked_ratio).unwrap();
1025+
write_last_pos_inflation_amount(storage, last_inflation_amount)
1026+
.unwrap();
1027+
let total_native_tokens_key =
1028+
minted_balance_key(&storage.get_native_token().unwrap());
1029+
storage
1030+
.write(&total_native_tokens_key, total_native_amount)
1031+
.unwrap();
1032+
}
9331033
}

crates/sdk/src/args.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2245,6 +2245,13 @@ pub struct QueryEffNativeSupply<C: NamadaTypes = SdkTypes> {
22452245
pub query: Query<C>,
22462246
}
22472247

2248+
/// Query estimate of staking rewards rate
2249+
#[derive(Clone, Debug)]
2250+
pub struct QueryStakingRewardsRate<C: NamadaTypes = SdkTypes> {
2251+
/// Common query args
2252+
pub query: Query<C>,
2253+
}
2254+
22482255
/// Query PoS to find a validator
22492256
#[derive(Clone, Debug)]
22502257
pub struct QueryFindValidator<C: NamadaTypes = SdkTypes> {

crates/sdk/src/queries/vp/token.rs

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
33
use namada_core::address::Address;
44
use namada_core::token;
5+
use namada_proof_of_stake::rewards::estimate_staking_reward_rate;
56
use namada_state::{DBIter, StorageHasher, DB};
67
use namada_token::{
7-
get_effective_total_native_supply, read_denom, read_total_supply,
8+
get_effective_total_native_supply, read_denom, read_total_supply, Dec,
89
};
910

1011
use crate::queries::RequestCtx;
@@ -13,6 +14,7 @@ router! {TOKEN,
1314
( "denomination" / [token: Address] ) -> Option<token::Denomination> = denomination,
1415
( "total_supply" / [token: Address] ) -> token::Amount = total_supply,
1516
( "effective_native_supply" ) -> token::Amount = effective_native_supply,
17+
( "staking_rewards_rate" ) -> Dec = staking_rewards_rate,
1618
}
1719

1820
/// Get the number of decimal places (in base 10) for a
@@ -51,6 +53,21 @@ where
5153
get_effective_total_native_supply(ctx.state)
5254
}
5355

56+
/// Get the effective total supply of the native token
57+
fn staking_rewards_rate<D, H, V, T>(
58+
ctx: RequestCtx<'_, D, H, V, T>,
59+
) -> namada_storage::Result<Dec>
60+
where
61+
D: 'static + DB + for<'iter> DBIter<'iter> + Sync,
62+
H: 'static + StorageHasher + Sync,
63+
{
64+
estimate_staking_reward_rate::<
65+
_,
66+
crate::token::Store<_>,
67+
crate::parameters::Store<_>,
68+
>(ctx.state)
69+
}
70+
5471
pub mod client_only_methods {
5572
use borsh::BorshDeserialize;
5673
use namada_core::address::Address;

crates/sdk/src/rpc.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ use namada_proof_of_stake::types::{
4646
};
4747
use namada_state::LastBlock;
4848
use namada_token::masp::MaspTokenRewardData;
49+
use namada_token::Dec;
4950
use namada_tx::data::{BatchedTxResult, DryRunResult, ResultCode, TxResult};
5051
use namada_tx::event::{Batch as BatchAttr, Code as CodeAttr};
5152
use serde::Serialize;
@@ -237,6 +238,15 @@ pub async fn get_effective_native_supply<C: Client + Sync>(
237238
)
238239
}
239240

241+
/// Query the effective total supply of the native token
242+
pub async fn get_staking_rewards_rate<C: Client + Sync>(
243+
client: &C,
244+
) -> Result<Dec, error::Error> {
245+
convert_response::<C, _>(
246+
RPC.vp().token().staking_rewards_rate(client).await,
247+
)
248+
}
249+
240250
/// Check if the given address is a known validator.
241251
pub async fn is_validator<C: namada_io::Client + Sync>(
242252
client: &C,

0 commit comments

Comments
 (0)