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
        collators_fraction >= 0.0 && collators_fraction <= 1.0,
41
        "collators_fraction must be between 0 and 1"
42
    );
43
3
    assert!(
44
3
        annual_inflation >= 0.0 && annual_inflation <= 1.0,
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 / (eras_per_year as f64)) - 1.0;
50
3

            
51
3
    // 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
3

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

            
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
1

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

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

            
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
1

            
79
1
    // "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
100

            
92
100
        let actual_era_inflation = supply as f64 / era_start_supply as f64 - 1.0;
93
100

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

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

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

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

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

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

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

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

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

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

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

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

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

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