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
13831
        fn on_initialize(_: BlockNumberFor<T>) -> Weight {
70
13831
            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
13831
            let not_distributed_rewards =
75
13831
                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
4928
                    CreditOf::<T>::zero()
90
                };
91

            
92
            // Get the number of chains at this block (tanssi + container chain blocks)
93
13831
            weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
94
13831
            let registered_para_ids = T::ContainerChains::current_container_chains();
95

            
96
13831
            let mut number_of_chains: BalanceOf<T> = (registered_para_ids.len() as u32).into();
97

            
98
            // We only add 1 extra chain to number_of_chains if we are
99
            // in a parachain context with an orchestrator configured.
100
13831
            if T::GetSelfChainBlockAuthor::get_block_author().is_some() {
101
6247
                number_of_chains.saturating_inc();
102
13831
            }
103

            
104
            // Only create new supply and rewards if number_of_chains is not zero.
105
13831
            if !number_of_chains.is_zero() {
106
                // Issue new supply
107
9405
                let new_supply =
108
9405
                    T::Currency::issue(T::InflationRate::get() * T::Currency::total_issuance());
109

            
110
                // Split staking reward portion
111
9405
                let total_rewards = T::RewardsPortion::get() * new_supply.peek();
112
9405
                let (rewards_credit, reminder_credit) = new_supply.split(total_rewards);
113

            
114
9405
                let rewards_per_chain: BalanceOf<T> = rewards_credit
115
9405
                    .peek()
116
9405
                    .checked_div(&number_of_chains)
117
9405
                    .unwrap_or_else(|| {
118
                        log::error!("Rewards per chain is zero");
119
                        BalanceOf::<T>::zero()
120
                    });
121
9405
                let (mut total_reminder, staking_rewards) = rewards_credit.split_merge(
122
9405
                    total_rewards % number_of_chains,
123
9405
                    (reminder_credit, CreditOf::<T>::zero()),
124
9405
                );
125

            
126
                // Deposit the new supply dedicated to rewards in the pending rewards account
127
                if let Err(undistributed_rewards) =
128
9405
                    T::Currency::resolve(&T::PendingRewardsAccount::get(), staking_rewards)
129
                {
130
                    total_reminder = total_reminder.merge(undistributed_rewards);
131
9405
                }
132

            
133
                // Keep track of chains to reward
134
9405
                ChainsToReward::<T>::put(ChainsToRewardValue {
135
9405
                    para_ids: registered_para_ids,
136
9405
                    rewards_per_chain,
137
9405
                });
138

            
139
                // Let the runtime handle the non-staking part
140
9405
                T::OnUnbalanced::on_unbalanced(not_distributed_rewards.merge(total_reminder));
141

            
142
                // We don't reward the orchestrator in solochain mode
143
9405
                if let Some(orchestrator_author) = T::GetSelfChainBlockAuthor::get_block_author() {
144
6247
                    weight.saturating_accrue(Self::reward_orchestrator_author(orchestrator_author));
145
9405
                }
146
4426
            }
147

            
148
13831
            weight
149
13831
        }
150
    }
151

            
152
    #[pallet::config]
153
    pub trait Config: frame_system::Config {
154
        type Currency: Inspect<Self::AccountId> + Balanced<Self::AccountId>;
155

            
156
        type ContainerChains: GetCurrentContainerChains;
157

            
158
        /// Get block author for self chain
159
        type GetSelfChainBlockAuthor: MaybeSelfChainBlockAuthor<Self::AccountId>;
160

            
161
        /// Inflation rate per orchestrator block (proportion of the total issuance)
162
        #[pallet::constant]
163
        type InflationRate: Get<Perbill>;
164

            
165
        /// What to do with the new supply not dedicated to staking
166
        type OnUnbalanced: OnUnbalanced<CreditOf<Self>>;
167

            
168
        /// The account that will store rewards waiting to be paid out
169
        #[pallet::constant]
170
        type PendingRewardsAccount: Get<Self::AccountId>;
171

            
172
        /// Staking rewards distribution implementation
173
        type StakingRewardsDistributor: DistributeRewards<Self::AccountId, CreditOf<Self>>;
174

            
175
        /// Proportion of the new supply dedicated to staking
176
        #[pallet::constant]
177
        type RewardsPortion: Get<Perbill>;
178
    }
179

            
180
    #[pallet::event]
181
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
182
    pub enum Event<T: Config> {
183
        /// Rewarding orchestrator author
184
        RewardedOrchestrator {
185
            account_id: T::AccountId,
186
            balance: BalanceOf<T>,
187
        },
188
        /// Rewarding container author
189
        RewardedContainer {
190
            account_id: T::AccountId,
191
            para_id: ParaId,
192
            balance: BalanceOf<T>,
193
        },
194
    }
195

            
196
    /// Container chains to reward per block
197
    #[pallet::storage]
198
    pub(super) type ChainsToReward<T: Config> =
199
        StorageValue<_, ChainsToRewardValue<T>, OptionQuery>;
200

            
201
    #[derive(
202
        Clone, Encode, Decode, PartialEq, sp_core::RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,
203
    )]
204
    #[scale_info(skip_type_params(T))]
205
    pub struct ChainsToRewardValue<T: Config> {
206
        pub para_ids: BoundedVec<
207
            ParaId,
208
            <T::ContainerChains as GetCurrentContainerChains>::MaxContainerChains,
209
        >,
210
        pub rewards_per_chain: BalanceOf<T>,
211
    }
212

            
213
    impl<T: Config> Pallet<T> {
214
6247
        fn reward_orchestrator_author(orchestrator_author: T::AccountId) -> Weight {
215
6247
            let mut total_weight = T::DbWeight::get().reads(1);
216
6247
            if let Some(chains_to_reward) = ChainsToReward::<T>::get() {
217
6247
                total_weight.saturating_accrue(T::DbWeight::get().reads(1));
218
6247
                match T::StakingRewardsDistributor::distribute_rewards(
219
6247
                    orchestrator_author.clone(),
220
6247
                    T::Currency::withdraw(
221
6247
                        &T::PendingRewardsAccount::get(),
222
6247
                        chains_to_reward.rewards_per_chain,
223
6247
                        Precision::BestEffort,
224
6247
                        Preservation::Expendable,
225
6247
                        Fortitude::Force,
226
6247
                    )
227
6247
                    .unwrap_or(CreditOf::<T>::zero()),
228
6247
                ) {
229
6181
                    Ok(frame_support::dispatch::PostDispatchInfo { actual_weight, .. }) => {
230
6181
                        Self::deposit_event(Event::RewardedOrchestrator {
231
6181
                            account_id: orchestrator_author,
232
6181
                            balance: chains_to_reward.rewards_per_chain,
233
6181
                        });
234

            
235
6181
                        if let Some(weight) = actual_weight {
236
6171
                            total_weight.saturating_accrue(weight)
237
10
                        }
238
                    }
239
66
                    Err(e) => {
240
66
                        log::debug!("Fail to distribute rewards: {:?}", e)
241
                    }
242
                }
243
            } else {
244
                panic!("ChainsToReward not filled");
245
            }
246
6247
            total_weight
247
6247
        }
248

            
249
        pub fn container_chains_to_reward() -> Option<ChainsToRewardValue<T>> {
250
            ChainsToReward::<T>::get()
251
        }
252
    }
253
}
254

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

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

            
302
129
            total_weight.saturating_accrue(T::DbWeight::get().writes(1));
303
            // Keep track of chains to reward
304
129
            ChainsToReward::<T>::put(container_chains_to_reward);
305
        } else {
306
            // TODO: why would ChainsToReward ever be None?
307
            log::warn!("ChainsToReward is None");
308
        }
309

            
310
129
        total_weight
311
129
    }
312

            
313
    #[cfg(feature = "runtime-benchmarks")]
314
    fn prepare_worst_case_for_bench(_a: &T::AccountId, _b: BlockNumber, para_id: ParaId) {
315
        // arbitrary amount to perform rewarding
316
        // we mint twice as much to the rewards account to make it possible
317
        let reward_amount = 1_000_000_000u32;
318
        let mint = reward_amount.saturating_mul(2);
319

            
320
        T::Currency::resolve(
321
            &T::PendingRewardsAccount::get(),
322
            T::Currency::issue(BalanceOf::<T>::from(mint)),
323
        )
324
        .expect("to mint tokens");
325

            
326
        ChainsToReward::<T>::put(ChainsToRewardValue {
327
            para_ids: alloc::vec![para_id].try_into().expect("to be in bound"),
328
            rewards_per_chain: BalanceOf::<T>::from(reward_amount),
329
        });
330
    }
331
}