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

            
23
pub use pallet::*;
24

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

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

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

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

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

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

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

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

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

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

            
97
28705
            // We only add 1 extra chain to number_of_chains if we are
98
28705
            // in a parachain context with an orchestrator configured.
99
28705
            if T::GetSelfChainBlockAuthor::get_block_author().is_some() {
100
25121
                number_of_chains = number_of_chains.saturating_add(1u32.into());
101
28705
            }
102

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

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

            
113
26577
                let rewards_per_chain: BalanceOf<T> = rewards_credit.peek() / number_of_chains;
114
26577
                let (mut total_reminder, staking_rewards) = rewards_credit.split_merge(
115
26577
                    total_rewards % number_of_chains,
116
26577
                    (reminder_credit, CreditOf::<T>::zero()),
117
26577
                );
118

            
119
                // Deposit the new supply dedicated to rewards in the pending rewards account
120
                if let Err(undistributed_rewards) =
121
26577
                    T::Currency::resolve(&T::PendingRewardsAccount::get(), staking_rewards)
122
                {
123
                    total_reminder = total_reminder.merge(undistributed_rewards);
124
26577
                }
125

            
126
                // Keep track of chains to reward
127
26577
                ChainsToReward::<T>::put(ChainsToRewardValue {
128
26577
                    para_ids: registered_para_ids,
129
26577
                    rewards_per_chain,
130
26577
                });
131
26577

            
132
26577
                // Let the runtime handle the non-staking part
133
26577
                T::OnUnbalanced::on_unbalanced(not_distributed_rewards.merge(total_reminder));
134

            
135
                // We don't reward the orchestrator in solochain mode
136
26577
                if let Some(orchestrator_author) = T::GetSelfChainBlockAuthor::get_block_author() {
137
25121
                    weight += Self::reward_orchestrator_author(orchestrator_author);
138
26577
                }
139
2128
            }
140

            
141
28705
            weight
142
28705
        }
143
    }
144

            
145
    #[pallet::config]
146
    pub trait Config: frame_system::Config {
147
        /// Overarching event type.
148
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
149

            
150
        type Currency: Inspect<Self::AccountId> + Balanced<Self::AccountId>;
151

            
152
        type ContainerChains: GetCurrentContainerChains;
153

            
154
        /// Get block author for self chain
155
        type GetSelfChainBlockAuthor: MaybeSelfChainBlockAuthor<Self::AccountId>;
156

            
157
        /// Inflation rate per orchestrator block (proportion of the total issuance)
158
        #[pallet::constant]
159
        type InflationRate: Get<Perbill>;
160

            
161
        /// What to do with the new supply not dedicated to staking
162
        type OnUnbalanced: OnUnbalanced<CreditOf<Self>>;
163

            
164
        /// The account that will store rewards waiting to be paid out
165
        #[pallet::constant]
166
        type PendingRewardsAccount: Get<Self::AccountId>;
167

            
168
        /// Staking rewards distribution implementation
169
        type StakingRewardsDistributor: DistributeRewards<Self::AccountId, CreditOf<Self>>;
170

            
171
        /// Proportion of the new supply dedicated to staking
172
        #[pallet::constant]
173
        type RewardsPortion: Get<Perbill>;
174
    }
175

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

            
192
    /// Container chains to reward per block
193
244658
    #[pallet::storage]
194
    pub(super) type ChainsToReward<T: Config> =
195
        StorageValue<_, ChainsToRewardValue<T>, OptionQuery>;
196

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

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

            
231
25088
                        if let Some(weight) = actual_weight {
232
25078
                            total_weight += weight
233
10
                        }
234
                    }
235
33
                    Err(e) => {
236
33
                        log::debug!("Fail to distribute rewards: {:?}", e)
237
                    }
238
                }
239
            } else {
240
                panic!("ChainsToReward not filled");
241
            }
242
25121
            total_weight
243
25121
        }
244

            
245
        pub fn container_chains_to_reward() -> Option<ChainsToRewardValue<T>> {
246
            ChainsToReward::<T>::get()
247
        }
248
    }
249
}
250

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

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

            
298
20816
            total_weight += T::DbWeight::get().writes(1);
299
20816
            // Keep track of chains to reward
300
20816
            ChainsToReward::<T>::put(container_chains_to_reward);
301
        } else {
302
            // TODO: why would ChainsToReward ever be None?
303
            log::warn!("ChainsToReward is None");
304
        }
305

            
306
20816
        total_weight
307
20816
    }
308

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

            
316
        T::Currency::resolve(
317
            &T::PendingRewardsAccount::get(),
318
            T::Currency::issue(BalanceOf::<T>::from(mint)),
319
        )
320
        .expect("to mint tokens");
321

            
322
        ChainsToReward::<T>::put(ChainsToRewardValue {
323
            para_ids: sp_std::vec![para_id].try_into().expect("to be in bound"),
324
            rewards_per_chain: BalanceOf::<T>::from(reward_amount),
325
        });
326
    }
327
}