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
use {
32
    dp_core::{BlockNumber, ParaId},
33
    frame_support::{
34
        pallet_prelude::*,
35
        traits::{
36
            fungible::{Balanced, Credit, Inspect},
37
            tokens::{Fortitude, Precision, Preservation},
38
            Imbalance, OnUnbalanced,
39
        },
40
    },
41
    frame_system::pallet_prelude::*,
42
    sp_runtime::{
43
        traits::{Get, Saturating, Zero},
44
        Perbill,
45
    },
46
    tp_traits::{
47
        AuthorNotingHook, DistributeRewards, GetCurrentContainerChains, MaybeSelfChainBlockAuthor,
48
    },
49
};
50

            
51
4657
#[frame_support::pallet]
52
pub mod pallet {
53
    use super::*;
54

            
55
    pub type BalanceOf<T> =
56
        <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;
57
    pub type CreditOf<T> = Credit<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
58

            
59
    /// Inflation rewards pallet.
60
590
    #[pallet::pallet]
61
    pub struct Pallet<T>(PhantomData<T>);
62

            
63
48226
    #[pallet::hooks]
64
    impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
65
27256
        fn on_initialize(_: BlockNumberFor<T>) -> Weight {
66
27256
            let mut weight = T::DbWeight::get().reads(1);
67

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

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

            
92
27256
            let mut number_of_chains: BalanceOf<T> = (registered_para_ids.len() as u32).into();
93
27256

            
94
27256
            // We only add 1 extra chain to number_of_chains if we are
95
27256
            // in a parachain context with an orchestrator configured.
96
27256
            if T::GetSelfChainBlockAuthor::get_block_author().is_some() {
97
24275
                number_of_chains = number_of_chains.saturating_add(1u32.into());
98
27256
            }
99

            
100
            // Only create new supply and rewards if number_of_chains is not zero.
101
27256
            if !number_of_chains.is_zero() {
102
                // Issue new supply
103
25360
                let new_supply =
104
25360
                    T::Currency::issue(T::InflationRate::get() * T::Currency::total_issuance());
105
25360

            
106
25360
                // Split staking reward portion
107
25360
                let total_rewards = T::RewardsPortion::get() * new_supply.peek();
108
25360
                let (rewards_credit, reminder_credit) = new_supply.split(total_rewards);
109
25360

            
110
25360
                let rewards_per_chain: BalanceOf<T> = rewards_credit.peek() / number_of_chains;
111
25360
                let (mut total_reminder, staking_rewards) = rewards_credit.split_merge(
112
25360
                    total_rewards % number_of_chains,
113
25360
                    (reminder_credit, CreditOf::<T>::zero()),
114
25360
                );
115

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

            
123
                // Keep track of chains to reward
124
25360
                ChainsToReward::<T>::put(ChainsToRewardValue {
125
25360
                    para_ids: registered_para_ids,
126
25360
                    rewards_per_chain,
127
25360
                });
128
25360

            
129
25360
                // Let the runtime handle the non-staking part
130
25360
                T::OnUnbalanced::on_unbalanced(not_distributed_rewards.merge(total_reminder));
131

            
132
                // We don't reward the orchestrator in solochain mode
133
25360
                if let Some(orchestrator_author) = T::GetSelfChainBlockAuthor::get_block_author() {
134
24275
                    weight += Self::reward_orchestrator_author(orchestrator_author);
135
25360
                }
136
1896
            }
137

            
138
27256
            weight
139
27256
        }
140
    }
141

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

            
147
        type Currency: Inspect<Self::AccountId> + Balanced<Self::AccountId>;
148

            
149
        type ContainerChains: GetCurrentContainerChains;
150

            
151
        /// Get block author for self chain
152
        type GetSelfChainBlockAuthor: MaybeSelfChainBlockAuthor<Self::AccountId>;
153

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

            
158
        /// What to do with the new supply not dedicated to staking
159
        type OnUnbalanced: OnUnbalanced<CreditOf<Self>>;
160

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

            
165
        /// Staking rewards distribution implementation
166
        type StakingRewardsDistributor: DistributeRewards<Self::AccountId, CreditOf<Self>>;
167

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

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

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

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

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

            
228
24242
                        if let Some(weight) = actual_weight {
229
24232
                            total_weight += weight
230
10
                        }
231
                    }
232
33
                    Err(e) => {
233
33
                        log::debug!("Fail to distribute rewards: {:?}", e)
234
                    }
235
                }
236
            } else {
237
                panic!("ChainsToReward not filled");
238
            }
239
24275
            total_weight
240
24275
        }
241

            
242
        pub fn container_chains_to_reward() -> Option<ChainsToRewardValue<T>> {
243
            ChainsToReward::<T>::get()
244
        }
245
    }
246
}
247

            
248
// This function should only be used to **reward** a container author.
249
// There will be no additional check other than checking if we have already
250
// rewarded this author for **in this tanssi block**
251
// Any additional check should be done in the calling function
252
// TODO: consider passing a vector here
253
impl<T: Config> AuthorNotingHook<T::AccountId> for Pallet<T> {
254
20222
    fn on_container_author_noted(
255
20222
        author: &T::AccountId,
256
20222
        _block_number: BlockNumber,
257
20222
        para_id: ParaId,
258
20222
    ) -> Weight {
259
20222
        let mut total_weight = T::DbWeight::get().reads_writes(1, 0);
260
        // We take chains to reward, to see what containers are left to reward
261
20222
        if let Some(mut container_chains_to_reward) = ChainsToReward::<T>::get() {
262
            // If we find the index is because we still have not rewarded it
263
20222
            if let Ok(index) = container_chains_to_reward.para_ids.binary_search(&para_id) {
264
                // we distribute rewards to the author
265
20221
                match T::StakingRewardsDistributor::distribute_rewards(
266
20221
                    author.clone(),
267
20221
                    T::Currency::withdraw(
268
20221
                        &T::PendingRewardsAccount::get(),
269
20221
                        container_chains_to_reward.rewards_per_chain,
270
20221
                        Precision::BestEffort,
271
20221
                        Preservation::Expendable,
272
20221
                        Fortitude::Force,
273
20221
                    )
274
20221
                    .unwrap_or(CreditOf::<T>::zero()),
275
20221
                ) {
276
20221
                    Ok(frame_support::dispatch::PostDispatchInfo { actual_weight, .. }) => {
277
20221
                        Self::deposit_event(Event::RewardedContainer {
278
20221
                            account_id: author.clone(),
279
20221
                            balance: container_chains_to_reward.rewards_per_chain,
280
20221
                            para_id,
281
20221
                        });
282
20221
                        if let Some(weight) = actual_weight {
283
20218
                            total_weight += weight
284
3
                        }
285
                    }
286
                    Err(e) => {
287
                        log::debug!("Fail to distribute rewards: {:?}", e)
288
                    }
289
                }
290
                // we remove the para id from container-chains to reward
291
                // this makes sure we dont reward it twice in the same block
292
20221
                container_chains_to_reward.para_ids.remove(index);
293
20221

            
294
20221
                total_weight += T::DbWeight::get().writes(1);
295
20221
                // Keep track of chains to reward
296
20221
                ChainsToReward::<T>::put(container_chains_to_reward);
297
1
            }
298
        }
299
20222
        total_weight
300
20222
    }
301
}