Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Estimate annual staking rewards rate #3816

Merged
merged 3 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading