sui_graphql_rpc/data/
apys.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use itertools::Itertools;
5use sui_types::sui_system_state::PoolTokenExchangeRate;
6
7/// Calculate an APY for a validator based on the exchange rates of the staking pool.
8///
9/// This is copied from the previous sui-json-rpc/governance_api crate, together with tests, and
10/// slightly altered to return one APY for each call instead of multiple ones.
11///
12/// See original code here:
13/// <https://github.com/MystenLabs/sui/blob/c3feec3ac3b626bf2fd40c668ba32be9c73e7528/crates/sui-json-rpc/src/governance_api.rs#L280>
14pub(crate) fn calculate_apy(
15    stake_subsidy_start_epoch: u64,
16    rates: &[(u64, PoolTokenExchangeRate)],
17) -> f64 {
18    // we start the apy calculation from the epoch when the stake subsidy starts
19    let exchange_rates = rates
20        .iter()
21        .filter_map(|(epoch, rate)| {
22            if epoch >= &stake_subsidy_start_epoch {
23                Some(rate)
24            } else {
25                None
26            }
27        })
28        .collect::<Vec<_>>();
29    let exchange_rates_size = exchange_rates.len();
30
31    // we need at least 2 data points to calculate apy
32    if exchange_rates_size >= 2 {
33        // rates are sorted by epoch in descending order.
34        let er_e = exchange_rates.iter().dropping(1);
35        // rate e+1
36        let er_e_1 = exchange_rates.iter().dropping_back(1);
37        let apys = er_e
38            .zip(er_e_1)
39            .map(apy_rate)
40            .filter(|apy| *apy > 0.0 && *apy < 0.1)
41            .take(30)
42            .collect::<Vec<_>>();
43
44        let apy_counts = apys.len() as f64;
45        apys.iter().sum::<f64>() / apy_counts
46    } else {
47        0.0
48    }
49}
50
51// APY_e = (ER_e+1 / ER_e) ^ 365
52pub(crate) fn apy_rate(
53    (rate_e, rate_e_1): (&&PoolTokenExchangeRate, &&PoolTokenExchangeRate),
54) -> f64 {
55    (rate_e.rate() / rate_e_1.rate()).powf(365.0) - 1.0
56}
57
58#[cfg(test)]
59mod tests {
60    use std::collections::BTreeMap;
61
62    use sui_json_rpc::governance_api::ValidatorExchangeRates;
63    use sui_types::base_types::{ObjectID, SuiAddress};
64
65    use super::*;
66
67    #[test]
68    fn test_apys_calculation_filter_outliers() {
69        // staking pool exchange rates extracted from mainnet
70        let file =
71            std::fs::File::open("src/unit_tests_data/validator_exchange_rates.json").unwrap();
72        let rates: BTreeMap<String, Vec<(u64, PoolTokenExchangeRate)>> =
73            serde_json::from_reader(file).unwrap();
74
75        let mut validator_exchange_rates = BTreeMap::new();
76        rates.into_iter().for_each(|(validator, rates)| {
77            let address = SuiAddress::random_for_testing_only();
78            validator_exchange_rates.insert(
79                address,
80                (
81                    validator,
82                    ValidatorExchangeRates {
83                        address,
84                        pool_id: ObjectID::random(),
85                        active: true,
86                        rates,
87                    },
88                ),
89            );
90        });
91
92        for (address, (validator, rates)) in &validator_exchange_rates {
93            let apy = calculate_apy(20, &rates.rates);
94            println!("{} {}: {}", validator, address, apy);
95            assert!(apy < 0.07)
96        }
97    }
98}