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
//! # Inflation Rewards Pallet
18
//!
19
//! This pallet handle native token inflation and rewards distribution.
20

            
21
#![cfg_attr(not(feature = "std"), no_std)]
22
extern crate alloc;
23

            
24
pub use pallet::*;
25

            
26
#[cfg(test)]
27
mod mock;
28

            
29
#[cfg(test)]
30
mod tests;
31

            
32
#[cfg(feature = "runtime-benchmarks")]
33
use tp_traits::BlockNumber;
34
use {
35
    dp_core::ParaId,
36
    frame_support::{
37
        pallet_prelude::*,
38
        traits::{
39
            fungible::{Balanced, Credit, Inspect},
40
            tokens::{Fortitude, Precision, Preservation},
41
            Imbalance, OnUnbalanced,
42
        },
43
    },
44
    frame_system::pallet_prelude::*,
45
    sp_runtime::{
46
        traits::{Get, Saturating, Zero},
47
        Perbill,
48
    },
49
    tp_traits::{
50
        AuthorNotingHook, AuthorNotingInfo, DistributeRewards, GetCurrentContainerChains,
51
        MaybeSelfChainBlockAuthor,
52
    },
53
};
54

            
55
#[frame_support::pallet]
56
pub mod pallet {
57
    use super::*;
58

            
59
    pub type BalanceOf<T> =
60
        <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
61
    pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
62

            
63
    /// Inflation rewards pallet.
64
    #[pallet::pallet]
65
    pub struct Pallet<T>(PhantomData<T>);
66

            
67
    #[pallet::hooks]
68
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
69
14216
        fn on_initialize(_: BlockNumberFor<T>) -> Weight {
70
14216
            let mut weight = T::DbWeight::get().reads(1);
71

            
72
            // Collect indistributed rewards, if any
73
            // Any parachain we have not rewarded is handled by onUnbalanced
74
14216
            let not_distributed_rewards =
75
14216
                if let Some(chains_to_reward) = ChainsToReward::<T>::take() {
76
                    // Collect and sum all undistributed rewards
77
8903
                    let rewards_not_distributed: BalanceOf<T> = chains_to_reward
78
8903
                        .rewards_per_chain
79
8903
                        .saturating_mul((chains_to_reward.para_ids.len() as u32).into());
80
8903
                    T::Currency::withdraw(
81
8903
                        &T::PendingRewardsAccount::get(),
82
8903
                        rewards_not_distributed,
83
8903
                        Precision::BestEffort,
84
8903
                        Preservation::Expendable,
85
8903
                        Fortitude::Force,
86
                    )
87
8903
                    .unwrap_or(CreditOf::<T>::zero())
88
                } else {
89
5313
                    CreditOf::<T>::zero()
90
                };
91

            
92
            // Get the number of chains at this block (tanssi + container chain blocks)
93
14216
            weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
94
            // TODO: this should be container_chains_with_collators to match author_noting
95
            // With the current impl we mint tokens for chains that don't have collators assigned
96
            // in the case where there aren't enough collators for all chains.
97
            // Or maybe that's desired, because it makes inflation depend on number of registered
98
            // chains.
99
14216
            let registered_para_ids =
100
14216
                bounded_vec_into_bounded_btree_set(T::ContainerChains::current_container_chains());
101

            
102
14216
            let mut number_of_chains: BalanceOf<T> = (registered_para_ids.len() as u32).into();
103

            
104
            // We only add 1 extra chain to number_of_chains if we are
105
            // in a parachain context with an orchestrator configured.
106
14216
            if T::GetSelfChainBlockAuthor::get_block_author().is_some() {
107
6247
                number_of_chains.saturating_inc();
108
14216
            }
109

            
110
            // Only create new supply and rewards if number_of_chains is not zero.
111
14216
            if !number_of_chains.is_zero() {
112
                // Issue new supply
113
9405
                let new_supply =
114
9405
                    T::Currency::issue(T::InflationRate::get() * T::Currency::total_issuance());
115

            
116
                // Split staking reward portion
117
9405
                let total_rewards = T::RewardsPortion::get() * new_supply.peek();
118
9405
                let (rewards_credit, reminder_credit) = new_supply.split(total_rewards);
119

            
120
9405
                let rewards_per_chain: BalanceOf<T> = rewards_credit
121
9405
                    .peek()
122
9405
                    .checked_div(&number_of_chains)
123
9405
                    .unwrap_or_else(|| {
124
                        log::error!("Rewards per chain is zero");
125
                        BalanceOf::<T>::zero()
126
                    });
127
9405
                let (mut total_reminder, staking_rewards) = rewards_credit.split_merge(
128
9405
                    total_rewards % number_of_chains,
129
9405
                    (reminder_credit, CreditOf::<T>::zero()),
130
9405
                );
131

            
132
                // Deposit the new supply dedicated to rewards in the pending rewards account
133
                if let Err(undistributed_rewards) =
134
9405
                    T::Currency::resolve(&T::PendingRewardsAccount::get(), staking_rewards)
135
                {
136
                    total_reminder = total_reminder.merge(undistributed_rewards);
137
9405
                }
138

            
139
                // Keep track of chains to reward
140
9405
                ChainsToReward::<T>::put(ChainsToRewardValue {
141
9405
                    para_ids: registered_para_ids,
142
9405
                    rewards_per_chain,
143
9405
                });
144

            
145
                // Let the runtime handle the non-staking part
146
9405
                T::OnUnbalanced::on_unbalanced(not_distributed_rewards.merge(total_reminder));
147

            
148
                // We don't reward the orchestrator in solochain mode
149
9405
                if let Some(orchestrator_author) = T::GetSelfChainBlockAuthor::get_block_author() {
150
6247
                    weight.saturating_accrue(Self::reward_orchestrator_author(orchestrator_author));
151
9405
                }
152
4811
            }
153

            
154
14216
            weight
155
14216
        }
156
    }
157

            
158
    #[pallet::config]
159
    pub trait Config: frame_system::Config {
160
        type Currency: Inspect<Self::AccountId> + Balanced<Self::AccountId>;
161

            
162
        type ContainerChains: GetCurrentContainerChains;
163

            
164
        /// Get block author for self chain
165
        type GetSelfChainBlockAuthor: MaybeSelfChainBlockAuthor<Self::AccountId>;
166

            
167
        /// Inflation rate per orchestrator block (proportion of the total issuance)
168
        #[pallet::constant]
169
        type InflationRate: Get<Perbill>;
170

            
171
        /// What to do with the new supply not dedicated to staking
172
        type OnUnbalanced: OnUnbalanced<CreditOf<Self>>;
173

            
174
        /// The account that will store rewards waiting to be paid out
175
        #[pallet::constant]
176
        type PendingRewardsAccount: Get<Self::AccountId>;
177

            
178
        /// Staking rewards distribution implementation
179
        type StakingRewardsDistributor: DistributeRewards<Self::AccountId, CreditOf<Self>>;
180

            
181
        /// Proportion of the new supply dedicated to staking
182
        #[pallet::constant]
183
        type RewardsPortion: Get<Perbill>;
184
    }
185

            
186
    #[pallet::event]
187
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
188
    pub enum Event<T: Config> {
189
        /// Rewarding orchestrator author
190
        RewardedOrchestrator {
191
            account_id: T::AccountId,
192
            balance: BalanceOf<T>,
193
        },
194
        /// Rewarding container author
195
        RewardedContainer {
196
            account_id: T::AccountId,
197
            para_id: ParaId,
198
            balance: BalanceOf<T>,
199
        },
200
    }
201

            
202
    /// Container chains to reward per block.
203
    /// This gets initialized to the list of chains that should be producing blocks.
204
    /// Then, in the `set_latest_author_data` inherent, the chains that actually have produced
205
    /// blocks are rewarded and removed from this list, in the `on_container_authors_noted` hook.
206
    /// Chains that have not produced blocks stay in this list, and their rewards get accumulated as
207
    /// `not_distributed_rewards` and handled by `OnUnbalanced` in the next block `on_initialize`.
208
    #[pallet::storage]
209
    pub(super) type ChainsToReward<T: Config> =
210
        StorageValue<_, ChainsToRewardValue<T>, OptionQuery>;
211

            
212
    #[derive(
213
        Clone, Encode, Decode, PartialEq, sp_core::RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,
214
    )]
215
    #[scale_info(skip_type_params(T))]
216
    pub struct ChainsToRewardValue<T: Config> {
217
        pub para_ids: BoundedBTreeSet<
218
            ParaId,
219
            <T::ContainerChains as GetCurrentContainerChains>::MaxContainerChains,
220
        >,
221
        pub rewards_per_chain: BalanceOf<T>,
222
    }
223

            
224
    impl<T: Config> Pallet<T> {
225
6247
        fn reward_orchestrator_author(orchestrator_author: T::AccountId) -> Weight {
226
6247
            let mut total_weight = T::DbWeight::get().reads(1);
227
6247
            if let Some(chains_to_reward) = ChainsToReward::<T>::get() {
228
6247
                total_weight.saturating_accrue(T::DbWeight::get().reads(1));
229
6247
                match T::StakingRewardsDistributor::distribute_rewards(
230
6247
                    orchestrator_author.clone(),
231
6247
                    T::Currency::withdraw(
232
6247
                        &T::PendingRewardsAccount::get(),
233
6247
                        chains_to_reward.rewards_per_chain,
234
6247
                        Precision::BestEffort,
235
6247
                        Preservation::Expendable,
236
6247
                        Fortitude::Force,
237
6247
                    )
238
6247
                    .unwrap_or(CreditOf::<T>::zero()),
239
6247
                ) {
240
6181
                    Ok(frame_support::dispatch::PostDispatchInfo { actual_weight, .. }) => {
241
6181
                        Self::deposit_event(Event::RewardedOrchestrator {
242
6181
                            account_id: orchestrator_author,
243
6181
                            balance: chains_to_reward.rewards_per_chain,
244
6181
                        });
245

            
246
6181
                        if let Some(weight) = actual_weight {
247
6171
                            total_weight.saturating_accrue(weight)
248
10
                        }
249
                    }
250
66
                    Err(e) => {
251
66
                        log::debug!("Fail to distribute rewards: {:?}", e)
252
                    }
253
                }
254
            } else {
255
                panic!("ChainsToReward not filled");
256
            }
257
6247
            total_weight
258
6247
        }
259

            
260
        pub fn container_chains_to_reward() -> Option<ChainsToRewardValue<T>> {
261
            ChainsToReward::<T>::get()
262
        }
263
    }
264
}
265

            
266
// This function should only be used to **reward** a container author.
267
// There will be no additional check other than checking if we have already
268
// rewarded this author for **in this tanssi block**
269
// Any additional check should be done in the calling function
270
impl<T: Config> AuthorNotingHook<T::AccountId> for Pallet<T> {
271
129
    fn on_container_authors_noted(info: &[AuthorNotingInfo<T::AccountId>]) -> Weight {
272
129
        let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
273
        // We take chains to reward, to see what containers are left to reward
274
129
        if let Some(mut container_chains_to_reward) = ChainsToReward::<T>::get() {
275
270
            for info in info {
276
141
                let author = &info.author;
277
141
                let para_id = info.para_id;
278

            
279
                // If we find the index is because we still have not rewarded it
280
                // this makes sure we dont reward it twice in the same block
281
141
                if container_chains_to_reward.para_ids.remove(&para_id) {
282
                    // we distribute rewards to the author
283
140
                    match T::StakingRewardsDistributor::distribute_rewards(
284
140
                        author.clone(),
285
140
                        T::Currency::withdraw(
286
140
                            &T::PendingRewardsAccount::get(),
287
140
                            container_chains_to_reward.rewards_per_chain,
288
140
                            Precision::BestEffort,
289
140
                            Preservation::Expendable,
290
140
                            Fortitude::Force,
291
140
                        )
292
140
                        .unwrap_or(CreditOf::<T>::zero()),
293
140
                    ) {
294
140
                        Ok(frame_support::dispatch::PostDispatchInfo { actual_weight, .. }) => {
295
140
                            Self::deposit_event(Event::RewardedContainer {
296
140
                                account_id: author.clone(),
297
140
                                balance: container_chains_to_reward.rewards_per_chain,
298
140
                                para_id,
299
140
                            });
300
140
                            if let Some(weight) = actual_weight {
301
137
                                total_weight.saturating_accrue(weight)
302
3
                            }
303
                        }
304
                        Err(e) => {
305
                            log::warn!("Fail to distribute rewards: {:?}", e)
306
                        }
307
                    }
308
1
                }
309
            }
310

            
311
129
            total_weight.saturating_accrue(T::DbWeight::get().writes(1));
312
            // Keep track of chains to reward
313
129
            ChainsToReward::<T>::put(container_chains_to_reward);
314
        } else {
315
            // Should never happen because `AuthorNotingInfo` is created from the same list of
316
            // para_ids as the one we use in pallet_inflation_rewards::OnInitialize, so they cannot
317
            // mismatch.
318
            // TODO: actually it is not the same list, but the author_noting list is a subset of
319
            // this one, so the warning cannot happen. See TODO in this pallet on_initialize
320
            log::warn!("AuthorNoting inherent tried to reward a chain that was not assigned collators. This is a bug.");
321
        }
322

            
323
129
        total_weight
324
129
    }
325

            
326
    #[cfg(feature = "runtime-benchmarks")]
327
    fn prepare_worst_case_for_bench(_a: &T::AccountId, _b: BlockNumber, para_id: ParaId) {
328
        // arbitrary amount to perform rewarding
329
        // we mint twice as much to the rewards account to make it possible
330
        let reward_amount = 1_000_000_000u32;
331
        let mint = reward_amount.saturating_mul(2);
332

            
333
        T::Currency::resolve(
334
            &T::PendingRewardsAccount::get(),
335
            T::Currency::issue(BalanceOf::<T>::from(mint)),
336
        )
337
        .expect("to mint tokens");
338

            
339
        ChainsToReward::<T>::put(ChainsToRewardValue {
340
            para_ids: alloc::collections::BTreeSet::from_iter([para_id])
341
                .try_into()
342
                .expect("to be in bound"),
343
            rewards_per_chain: BalanceOf::<T>::from(reward_amount),
344
        });
345
    }
346
}
347

            
348
/// Convert a `BoundedVec` into a `BoundedBTreeSet` with the same bound.
349
// TODO: upstream this into BoundedVec crate
350
14216
fn bounded_vec_into_bounded_btree_set<T: core::cmp::Ord + core::fmt::Debug, S: Get<u32>>(
351
14216
    x: BoundedVec<T, S>,
352
14216
) -> BoundedBTreeSet<T, S> {
353
14216
    let mut set = BoundedBTreeSet::default();
354

            
355
29333
    for item in x {
356
15117
        set.try_insert(item)
357
15117
            .expect("insert must have enough space because vec and set use the same type as bound");
358
15117
    }
359

            
360
14216
    set
361
14216
}