diff --git a/crates/apps_lib/src/cli.rs b/crates/apps_lib/src/cli.rs index 8e10945253..894a4b6dd5 100644 --- a/crates/apps_lib/src/cli.rs +++ b/crates/apps_lib/src/cli.rs @@ -298,6 +298,7 @@ pub mod cmds { .subcommand(QueryMetaData::def().display_order(5)) .subcommand(QueryTotalSupply::def().display_order(5)) .subcommand(QueryEffNativeSupply::def().display_order(5)) + .subcommand(QueryStakingRewardsRate::def().display_order(5)) // Actions .subcommand(SignTx::def().display_order(6)) .subcommand(ShieldedSync::def().display_order(6)) @@ -373,6 +374,8 @@ pub mod cmds { Self::parse_with_ctx(matches, QueryTotalSupply); let query_native_supply = Self::parse_with_ctx(matches, QueryEffNativeSupply); + let query_staking_rewards_rate = + Self::parse_with_ctx(matches, QueryStakingRewardsRate); let query_find_validator = Self::parse_with_ctx(matches, QueryFindValidator); let query_result = Self::parse_with_ctx(matches, QueryResult); @@ -449,6 +452,7 @@ pub mod cmds { .or(query_metadata) .or(query_total_supply) .or(query_native_supply) + .or(query_staking_rewards_rate) .or(query_account) .or(sign_tx) .or(shielded_sync) @@ -534,6 +538,7 @@ pub mod cmds { QueryDelegations(QueryDelegations), QueryTotalSupply(QueryTotalSupply), QueryEffNativeSupply(QueryEffNativeSupply), + QueryStakingRewardsRate(QueryStakingRewardsRate), QueryFindValidator(QueryFindValidator), QueryRawBytes(QueryRawBytes), QueryProposal(QueryProposal), @@ -2118,6 +2123,36 @@ pub mod cmds { } } + #[derive(Clone, Debug)] + pub struct QueryStakingRewardsRate( + pub args::QueryStakingRewardsRate, + ); + + impl SubCmd for QueryStakingRewardsRate { + const CMD: &'static str = "staking-rewards-rate"; + + fn parse(matches: &ArgMatches) -> Option + where + Self: Sized, + { + matches.subcommand_matches(Self::CMD).map(|matches| { + QueryStakingRewardsRate(args::QueryStakingRewardsRate::parse( + matches, + )) + }) + } + + fn def() -> App { + App::new(Self::CMD) + .about(wrap!( + "Query the latest estimate of the staking rewards rate \ + based on the most recent minted inflation amount at the \ + last epoch change." + )) + .add_args::>() + } + } + #[derive(Clone, Debug)] pub struct QueryFindValidator(pub args::QueryFindValidator); @@ -7157,6 +7192,32 @@ pub mod args { } } + impl Args for QueryStakingRewardsRate { + fn parse(matches: &ArgMatches) -> Self { + let query = Query::parse(matches); + Self { query } + } + + fn def(app: App) -> App { + app.add_args::>() + } + } + + impl CliToSdk> + for QueryStakingRewardsRate + { + type Error = std::convert::Infallible; + + fn to_sdk( + self, + ctx: &mut Context, + ) -> Result, Self::Error> { + Ok(QueryStakingRewardsRate:: { + query: self.query.to_sdk(ctx)?, + }) + } + } + impl Args for QueryFindValidator { fn parse(matches: &ArgMatches) -> Self { let query = Query::parse(matches); diff --git a/crates/apps_lib/src/cli/client.rs b/crates/apps_lib/src/cli/client.rs index 01a50d3860..a11f6b785b 100644 --- a/crates/apps_lib/src/cli/client.rs +++ b/crates/apps_lib/src/cli/client.rs @@ -661,6 +661,19 @@ impl CliApi { let namada = ctx.to_sdk(client, io); rpc::query_effective_native_supply(&namada).await; } + Sub::QueryStakingRewardsRate(QueryStakingRewardsRate( + args, + )) => { + let chain_ctx = ctx.borrow_mut_chain_or_exit(); + let ledger_address = + chain_ctx.get(&args.query.ledger_address); + let client = client.unwrap_or_else(|| { + C::from_tendermint_address(&ledger_address) + }); + client.wait_until_node_is_synced(&io).await?; + let namada = ctx.to_sdk(client, io); + rpc::query_staking_rewards_rate(&namada).await; + } Sub::QueryFindValidator(QueryFindValidator(args)) => { let chain_ctx = ctx.borrow_mut_chain_or_exit(); let ledger_address = diff --git a/crates/apps_lib/src/client/rpc.rs b/crates/apps_lib/src/client/rpc.rs index 5f0cb7f48c..bb44f82881 100644 --- a/crates/apps_lib/src/client/rpc.rs +++ b/crates/apps_lib/src/client/rpc.rs @@ -15,6 +15,7 @@ use namada_sdk::address::{Address, InternalAddress, MASP}; use namada_sdk::chain::{BlockHeight, Epoch}; use namada_sdk::collections::{HashMap, HashSet}; use namada_sdk::control_flow::time::{Duration, Instant}; +use namada_sdk::dec::Dec; use namada_sdk::events::Event; use namada_sdk::governance::parameters::GovernanceParameters; use namada_sdk::governance::pgf::parameters::PgfParameters; @@ -1364,6 +1365,21 @@ pub async fn query_effective_native_supply(context: &N) { display_line!(context.io(), "nam: {}", native_supply.to_string_native()); } +/// Query the staking rewards rate estimate +pub async fn query_staking_rewards_rate(context: &N) { + let rewards_rate = unwrap_client_response::( + RPC.vp() + .token() + .staking_rewards_rate(context.client()) + .await, + ); + display_line!( + context.io(), + "Current annual staking rewards rate: {}", + rewards_rate + ); +} + /// Query a validator's state information pub async fn query_and_print_validator_state( context: &impl Namada, diff --git a/crates/proof_of_stake/src/rewards.rs b/crates/proof_of_stake/src/rewards.rs index 1efb6040a4..45e6422362 100644 --- a/crates/proof_of_stake/src/rewards.rs +++ b/crates/proof_of_stake/src/rewards.rs @@ -656,6 +656,41 @@ where Ok(storage.read::(&key)?.unwrap_or_default()) } +/// Compute an estimation of the most recent staking rewards rate. +pub fn estimate_staking_reward_rate( + storage: &S, +) -> Result +where + S: StorageRead, + Parameters: parameters::Read, + Token: trans_token::Read + trans_token::Write, +{ + // Get needed data in desired form + let total_native_tokens = + Token::get_effective_total_native_supply(storage)?; + let last_staked_ratio = read_last_staked_ratio(storage)? + .expect("Last staked ratio should exist in PoS storage"); + let last_inflation_amount = read_last_pos_inflation_amount(storage)? + .expect("Last inflation amount should exist in PoS storage"); + let epochs_per_year: u64 = Parameters::epochs_per_year(storage)?; + + let total_native_tokens = + Dec::try_from(total_native_tokens).into_storage_result()?; + let last_inflation_amount = + Dec::try_from(last_inflation_amount).into_storage_result()?; + + // Estimate annual inflation rate + let est_inflation_rate = checked!( + last_inflation_amount * epochs_per_year / total_native_tokens + )?; + + // Estimate annual staking rewards rate + let est_staking_reward_rate = + checked!(est_inflation_rate / last_staked_ratio)?; + + Ok(est_staking_reward_rate) +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/sdk/src/args.rs b/crates/sdk/src/args.rs index cf063f5d8e..f7349c40ea 100644 --- a/crates/sdk/src/args.rs +++ b/crates/sdk/src/args.rs @@ -2245,6 +2245,13 @@ pub struct QueryEffNativeSupply { pub query: Query, } +/// Query estimate of staking rewards rate +#[derive(Clone, Debug)] +pub struct QueryStakingRewardsRate { + /// Common query args + pub query: Query, +} + /// Query PoS to find a validator #[derive(Clone, Debug)] pub struct QueryFindValidator { diff --git a/crates/sdk/src/queries/vp/token.rs b/crates/sdk/src/queries/vp/token.rs index b0f151eaa1..caea8392f8 100644 --- a/crates/sdk/src/queries/vp/token.rs +++ b/crates/sdk/src/queries/vp/token.rs @@ -2,9 +2,10 @@ use namada_core::address::Address; use namada_core::token; +use namada_proof_of_stake::rewards::estimate_staking_reward_rate; use namada_state::{DBIter, StorageHasher, DB}; use namada_token::{ - get_effective_total_native_supply, read_denom, read_total_supply, + get_effective_total_native_supply, read_denom, read_total_supply, Dec, }; use crate::queries::RequestCtx; @@ -13,6 +14,7 @@ router! {TOKEN, ( "denomination" / [token: Address] ) -> Option = denomination, ( "total_supply" / [token: Address] ) -> token::Amount = total_supply, ( "effective_native_supply" ) -> token::Amount = effective_native_supply, + ( "staking_rewards_rate" ) -> Dec = staking_rewards_rate, } /// Get the number of decimal places (in base 10) for a @@ -51,6 +53,21 @@ where get_effective_total_native_supply(ctx.state) } +/// Get the effective total supply of the native token +fn staking_rewards_rate( + ctx: RequestCtx<'_, D, H, V, T>, +) -> namada_storage::Result +where + D: 'static + DB + for<'iter> DBIter<'iter> + Sync, + H: 'static + StorageHasher + Sync, +{ + estimate_staking_reward_rate::< + _, + crate::token::Store<_>, + crate::parameters::Store<_>, + >(ctx.state) +} + pub mod client_only_methods { use borsh::BorshDeserialize; use namada_core::address::Address; diff --git a/crates/sdk/src/rpc.rs b/crates/sdk/src/rpc.rs index 781ecc41fa..e1e0fd7589 100644 --- a/crates/sdk/src/rpc.rs +++ b/crates/sdk/src/rpc.rs @@ -46,6 +46,7 @@ use namada_proof_of_stake::types::{ }; use namada_state::LastBlock; use namada_token::masp::MaspTokenRewardData; +use namada_token::Dec; use namada_tx::data::{BatchedTxResult, DryRunResult, ResultCode, TxResult}; use namada_tx::event::{Batch as BatchAttr, Code as CodeAttr}; use serde::Serialize; @@ -237,6 +238,15 @@ pub async fn get_effective_native_supply( ) } +/// Query the effective total supply of the native token +pub async fn get_staking_rewards_rate( + client: &C, +) -> Result { + convert_response::( + RPC.vp().token().staking_rewards_rate(client).await, + ) +} + /// Check if the given address is a known validator. pub async fn is_validator( client: &C,