Skip to content

Commit

Permalink
Merge pull request #3816 from anoma/brent/estimate-staking-reward-rate
Browse files Browse the repository at this point in the history
Estimate annual staking rewards rate
  • Loading branch information
mergify[bot] authored Sep 15, 2024
2 parents f7830f6 + 8c4e722 commit e603759
Show file tree
Hide file tree
Showing 9 changed files with 239 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
- Adds an SDK and CLI tool to estimate the latest annual staking rewards rate.
([\#3816](https://github.com/anoma/namada/pull/3816))
61 changes: 61 additions & 0 deletions crates/apps_lib/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -534,6 +538,7 @@ pub mod cmds {
QueryDelegations(QueryDelegations),
QueryTotalSupply(QueryTotalSupply),
QueryEffNativeSupply(QueryEffNativeSupply),
QueryStakingRewardsRate(QueryStakingRewardsRate),
QueryFindValidator(QueryFindValidator),
QueryRawBytes(QueryRawBytes),
QueryProposal(QueryProposal),
Expand Down Expand Up @@ -2118,6 +2123,36 @@ pub mod cmds {
}
}

#[derive(Clone, Debug)]
pub struct QueryStakingRewardsRate(
pub args::QueryStakingRewardsRate<args::CliTypes>,
);

impl SubCmd for QueryStakingRewardsRate {
const CMD: &'static str = "staking-rewards-rate";

fn parse(matches: &ArgMatches) -> Option<Self>
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::<args::QueryStakingRewardsRate<args::CliTypes>>()
}
}

#[derive(Clone, Debug)]
pub struct QueryFindValidator(pub args::QueryFindValidator<args::CliTypes>);

Expand Down Expand Up @@ -7157,6 +7192,32 @@ pub mod args {
}
}

impl Args for QueryStakingRewardsRate<CliTypes> {
fn parse(matches: &ArgMatches) -> Self {
let query = Query::parse(matches);
Self { query }
}

fn def(app: App) -> App {
app.add_args::<Query<CliTypes>>()
}
}

impl CliToSdk<QueryStakingRewardsRate<SdkTypes>>
for QueryStakingRewardsRate<CliTypes>
{
type Error = std::convert::Infallible;

fn to_sdk(
self,
ctx: &mut Context,
) -> Result<QueryStakingRewardsRate<SdkTypes>, Self::Error> {
Ok(QueryStakingRewardsRate::<SdkTypes> {
query: self.query.to_sdk(ctx)?,
})
}
}

impl Args for QueryFindValidator<CliTypes> {
fn parse(matches: &ArgMatches) -> Self {
let query = Query::parse(matches);
Expand Down
13 changes: 13 additions & 0 deletions crates/apps_lib/src/cli/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
16 changes: 16 additions & 0 deletions crates/apps_lib/src/client/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1364,6 +1365,21 @@ pub async fn query_effective_native_supply<N: Namada>(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<N: Namada>(context: &N) {
let rewards_rate = unwrap_client_response::<N::Client, Dec>(
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,
Expand Down
104 changes: 102 additions & 2 deletions crates/proof_of_stake/src/rewards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -656,11 +656,52 @@ where
Ok(storage.read::<token::Amount>(&key)?.unwrap_or_default())
}

/// Compute an estimation of the most recent staking rewards rate.
pub fn estimate_staking_reward_rate<S, Token, Parameters>(
storage: &S,
) -> Result<Dec>
where
S: StorageRead,
Parameters: parameters::Read<S>,
Token: trans_token::Read<S> + trans_token::Write<S>,
{
// 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;

use namada_parameters::storage::get_epochs_per_year_key;
use namada_state::testing::TestState;
use namada_trans_token::storage_key::minted_balance_key;
use storage::write_pos_params;

use super::*;
use crate::OwnedPosParams;

#[test]
fn test_inflation_calc_up() {
Expand Down Expand Up @@ -842,10 +883,19 @@ mod tests {

#[test]
fn test_pos_inflation_playground() {
let mut storage = TestState::default();
let gov_params =
namada_governance::parameters::GovernanceParameters::default();
gov_params.init_storage(&mut storage).unwrap();
write_pos_params(&mut storage, &OwnedPosParams::default()).unwrap();

let epochs_per_year = 365_u64;
let epy_key = get_epochs_per_year_key();
storage.write(&epy_key, epochs_per_year).unwrap();

let init_locked_ratio = Dec::from_str("0.1").unwrap();
let mut last_locked_ratio = init_locked_ratio;

let total_native_tokens = 1_000_000_000_u64;
let locked_amount = u64::try_from(
(init_locked_ratio * total_native_tokens).to_uint().unwrap(),
Expand All @@ -856,6 +906,13 @@ mod tests {
let mut total_native_tokens =
token::Amount::native_whole(total_native_tokens);

update_state_for_pos_playground(
&mut storage,
last_locked_ratio,
last_inflation_amount,
total_native_tokens,
);

let max_reward_rate = Dec::from_str("0.1").unwrap();
let target_ratio = Dec::from_str("0.66666666").unwrap();
let p_gain_nom = Dec::from_str("0.25").unwrap();
Expand All @@ -882,17 +939,42 @@ mod tests {
let locked_ratio = Dec::try_from(locked_amount).unwrap()
/ Dec::try_from(total_native_tokens).unwrap();

let rate = Dec::try_from(inflation).unwrap()
let inflation_rate = Dec::try_from(inflation).unwrap()
* Dec::from(epochs_per_year)
/ Dec::try_from(total_native_tokens).unwrap();
let staking_rate = inflation_rate / locked_ratio;

println!(
"Round {round}: Locked ratio: {locked_ratio}, inflation rate: \
{rate}",
{inflation_rate}, staking rate: {staking_rate}",
);

last_inflation_amount = inflation;
total_native_tokens += inflation;
last_locked_ratio = locked_ratio;
update_state_for_pos_playground(
&mut storage,
last_locked_ratio,
last_inflation_amount,
total_native_tokens,
);

let query_staking_rate = estimate_staking_reward_rate::<
_,
namada_trans_token::Store<_>,
namada_parameters::Store<_>,
>(&storage)
.unwrap();
// println!(" ----> Query staking rate: {query_staking_rate}");
if !staking_rate.is_zero() && !query_staking_rate.is_zero() {
let ratio = staking_rate / query_staking_rate;
let residual = ratio.abs_diff(Dec::one()).unwrap();
assert!(residual < Dec::from_str("0.001").unwrap());
// println!(
// " ----> Ratio: {}\n",
// staking_rate / query_staking_rate
// );
}

// if rate.abs_diff(&controller.max_reward_rate)
// < Dec::from_str("0.01").unwrap()
Expand Down Expand Up @@ -930,4 +1012,22 @@ mod tests {
// );
}
}

fn update_state_for_pos_playground<S>(
storage: &mut S,
last_staked_ratio: Dec,
last_inflation_amount: token::Amount,
total_native_amount: token::Amount,
) where
S: StorageRead + StorageWrite,
{
write_last_staked_ratio(storage, last_staked_ratio).unwrap();
write_last_pos_inflation_amount(storage, last_inflation_amount)
.unwrap();
let total_native_tokens_key =
minted_balance_key(&storage.get_native_token().unwrap());
storage
.write(&total_native_tokens_key, total_native_amount)
.unwrap();
}
}
7 changes: 7 additions & 0 deletions crates/sdk/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2245,6 +2245,13 @@ pub struct QueryEffNativeSupply<C: NamadaTypes = SdkTypes> {
pub query: Query<C>,
}

/// Query estimate of staking rewards rate
#[derive(Clone, Debug)]
pub struct QueryStakingRewardsRate<C: NamadaTypes = SdkTypes> {
/// Common query args
pub query: Query<C>,
}

/// Query PoS to find a validator
#[derive(Clone, Debug)]
pub struct QueryFindValidator<C: NamadaTypes = SdkTypes> {
Expand Down
19 changes: 18 additions & 1 deletion crates/sdk/src/queries/vp/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -13,6 +14,7 @@ router! {TOKEN,
( "denomination" / [token: Address] ) -> Option<token::Denomination> = 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
Expand Down Expand Up @@ -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<D, H, V, T>(
ctx: RequestCtx<'_, D, H, V, T>,
) -> namada_storage::Result<Dec>
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;
Expand Down
10 changes: 10 additions & 0 deletions crates/sdk/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -237,6 +238,15 @@ pub async fn get_effective_native_supply<C: Client + Sync>(
)
}

/// Query the effective total supply of the native token
pub async fn get_staking_rewards_rate<C: Client + Sync>(
client: &C,
) -> Result<Dec, error::Error> {
convert_response::<C, _>(
RPC.vp().token().staking_rewards_rate(client).await,
)
}

/// Check if the given address is a known validator.
pub async fn is_validator<C: namada_io::Client + Sync>(
client: &C,
Expand Down
Loading

0 comments on commit e603759

Please sign in to comment.