1
// Copyright (C) Moondance Labs Ltd.
2
// This file is part of Tanssi.
3

            
4
// Tanssi is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8

            
9
// Tanssi is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13

            
14
// You should have received a copy of the GNU General Public License
15
// along with Tanssi.  If not, see <http://www.gnu.org/licenses/>
16

            
17
use crate::{
18
    tests::common::ExtBuilder, Balances, CollatorsInflationRatePerBlock, EpochDurationInBlocks,
19
    Perbill, RewardsPortion, Runtime, SessionsPerEra, ValidatorsInflationRatePerEra, DAYS,
20
};
21

            
22
#[derive(Debug)]
23
struct InflationRates {
24
    era_inflation: f64,
25
    collators_block_inflation: f64,
26
    validators_era_inflation: f64,
27
}
28

            
29
/// Computes the following inflation rates:
30
/// - Collators inflation rate (per block)
31
/// - Validators inflation rate (per era)
32
///   Era inflation is split between collators and validators using collators_fraction
33
3
fn compute_inflation_rates(
34
3
    annual_inflation: f64,
35
3
    collators_fraction: f64,
36
3
    eras_per_year: u32,
37
3
    blocks_per_era: u32,
38
3
) -> InflationRates {
39
3
    assert!(
40
3
        (0.0..=1.0).contains(&collators_fraction),
41
        "collators_fraction must be between 0 and 1"
42
    );
43
3
    assert!(
44
3
        (0.0..=1.0).contains(&annual_inflation),
45
        "annual_inflation is a % and should be between 0 (0%) and 1 (100%)"
46
    );
47

            
48
    // Compute era inflation based on annual inflation
49
3
    let era_inflation = (1.0 + annual_inflation).powf(1.0 / f64::from(eras_per_year)) - 1.0;
50

            
51
    // Compute collators and validators era inflation
52
3
    let collators_era_inflation = (1.0 + era_inflation).powf(collators_fraction) - 1.0;
53
3
    let validators_era_inflation = (1.0 + era_inflation).powf(1.0 - collators_fraction) - 1.0;
54

            
55
    // Compute collator block inflation
56
3
    let collators_block_inflation =
57
3
        (1.0 + collators_era_inflation).powf(1.0 / f64::from(blocks_per_era)) - 1.0;
58

            
59
3
    InflationRates {
60
3
        era_inflation,
61
3
        collators_block_inflation,
62
3
        validators_era_inflation,
63
3
    }
64
3
}
65

            
66
#[test]
67
1
fn formula_is_sound() {
68
1
    let eras_per_year = 100;
69
1
    let blocks_per_era = 100;
70
1
    let annual_inflation = 0.1; // 10%
71

            
72
1
    let rates = compute_inflation_rates(annual_inflation, 0.6, eras_per_year, blocks_per_era);
73

            
74
1
    println!("Rates: {rates:?}");
75

            
76
1
    let col_inf = Perbill::from_float(rates.collators_block_inflation);
77
1
    let val_inf = Perbill::from_float(rates.validators_era_inflation);
78

            
79
    // "big" supply to reduce rounding errors
80
1
    let initial_supply = 100_000_000_000_000_000u128;
81
1
    let mut supply = initial_supply;
82

            
83
100
    for era in 0..eras_per_year {
84
100
        let era_start_supply = supply;
85

            
86
10000
        for _block in 0..blocks_per_era {
87
10000
            supply += col_inf * supply;
88
10000
        }
89

            
90
100
        supply += val_inf * supply;
91

            
92
        #[allow(clippy::cast_precision_loss)]
93
100
        let actual_era_inflation = supply as f64 / era_start_supply as f64 - 1.0;
94

            
95
100
        println!("Era {era}: Supply {supply}, Actual inf: {actual_era_inflation}");
96
100
        assert!((actual_era_inflation - rates.era_inflation).abs() < 0.00001);
97
    }
98

            
99
    #[allow(clippy::cast_precision_loss)]
100
1
    let actual_annual_inflation = supply as f64 / initial_supply as f64 - 1.0;
101
1
    println!("Initial supply: {initial_supply}");
102
1
    println!("Final supply:   {supply}");
103
1
    println!("Actual annual inflation: {actual_annual_inflation}");
104
1
    assert!((actual_annual_inflation - annual_inflation).abs() < 0.00001);
105
1
}
106

            
107
2
fn runtime_inflations_values_are_correct_prod_or_fast(prod: bool) {
108
2
    ExtBuilder::default().build().execute_with(|| {
109
2
        let sessions_per_era = SessionsPerEra::prod_if(prod);
110
2
        let blocks_per_session = EpochDurationInBlocks::prod_if(prod);
111
2
        let blocks_per_era = blocks_per_session * sessions_per_era;
112
2
        let eras_per_year = (365 * DAYS) / blocks_per_era;
113

            
114
        // Annual inflation as float [0 - 1]
115
        // 7.5%
116
2
        let annual_inflation = 0.075;
117
        // Collators+staking get 3.5% out of the 100%,
118
        // so 3.5 / 7.5 as a fraction
119
        // Rest goes to validators (4%)
120
2
        let collators_fraction = 3.5 / 7.5;
121

            
122
2
        let rates = compute_inflation_rates(
123
2
            annual_inflation,
124
2
            collators_fraction,
125
2
            eras_per_year,
126
2
            blocks_per_era,
127
        );
128
2
        println!("{rates:?}");
129

            
130
2
        let col_inf = Perbill::from_float(rates.collators_block_inflation);
131
2
        let val_inf = Perbill::from_float(rates.validators_era_inflation);
132

            
133
2
        assert_eq!(
134
2
            CollatorsInflationRatePerBlock::prod_if(prod),
135
            col_inf,
136
            "Collators inflation didn't match"
137
        );
138
2
        assert_eq!(
139
2
            ValidatorsInflationRatePerEra::prod_if(prod),
140
            val_inf,
141
            "Validators inflation didn't match"
142
        );
143

            
144
2
        assert!(
145
2
            CollatorsInflationRatePerBlock::prod_if(prod)
146
2
                < ValidatorsInflationRatePerEra::prod_if(prod),
147
            "block inflation should be less than era inflation, are they swapped?"
148
        );
149

            
150
        // CollatorsInflationRatePerBlock must be used in the pallet that rewards
151
        // container chains collators.
152
2
        assert_eq!(
153
2
            <Runtime as pallet_inflation_rewards::Config>::InflationRate::get(),
154
2
            CollatorsInflationRatePerBlock::get(),
155
        );
156

            
157
        // ValidatorsInflationRatePerEra must be used in the pallet that rewards
158
        // external validators. In this pallet the getter directly provides the inflated
159
        // amount.
160
2
        assert_eq!(
161
2
            <Runtime as pallet_external_validators_rewards::Config>::EraInflationProvider::get(),
162
2
            ValidatorsInflationRatePerEra::get() * Balances::total_issuance(),
163
        );
164
2
    })
165
2
}
166

            
167
#[test]
168
1
fn runtime_inflations_values_are_correct_in_prod() {
169
1
    runtime_inflations_values_are_correct_prod_or_fast(true)
170
1
}
171

            
172
#[test]
173
1
fn runtime_inflations_values_are_correct_in_fast() {
174
1
    runtime_inflations_values_are_correct_prod_or_fast(false)
175
1
}
176

            
177
#[test]
178
1
fn inflation_table() {
179
    // All values in percentages [0 - 100]
180
1
    let total = 7.5;
181
1
    let validators = 4.0;
182
1
    let collators = 2.0;
183
1
    let parachain_bond = 1.5;
184

            
185
1
    assert_eq!(validators + collators + parachain_bond, total);
186
1
    assert_eq!(
187
1
        RewardsPortion::get(),
188
1
        Perbill::from_float(collators / (collators + parachain_bond))
189
    );
190
1
}