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},
44
        Perbill,
45
    },
46
    tp_traits::{AuthorNotingHook, DistributeRewards, GetCurrentContainerChains},
47
};
48

            
49
1936
#[frame_support::pallet]
50
pub mod pallet {
51
    use super::*;
52

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

            
57
    /// Inflation rewards pallet.
58
182
    #[pallet::pallet]
59
    #[pallet::without_storage_info]
60
    pub struct Pallet<T>(PhantomData<T>);
61

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

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

            
87
            // Get the number of chains at this block (tanssi + container chain blocks)
88
10115
            weight += T::DbWeight::get().reads_writes(1, 1);
89
10115
            let registered_para_ids = T::ContainerChains::current_container_chains();
90
10115
            let number_of_chains: BalanceOf<T> =
91
10115
                ((registered_para_ids.len() as u32).saturating_add(1)).into();
92
10115

            
93
10115
            // Issue new supply
94
10115
            let new_supply =
95
10115
                T::Currency::issue(T::InflationRate::get() * T::Currency::total_issuance());
96
10115

            
97
10115
            // Split staking reward portion
98
10115
            let total_rewards = T::RewardsPortion::get() * new_supply.peek();
99
10115
            let (rewards_credit, reminder_credit) = new_supply.split(total_rewards);
100
10115

            
101
10115
            let rewards_per_chain: BalanceOf<T> = rewards_credit.peek() / number_of_chains;
102
10115
            let (mut total_reminder, staking_rewards) = rewards_credit.split_merge(
103
10115
                total_rewards % number_of_chains,
104
10115
                (reminder_credit, CreditOf::<T>::zero()),
105
10115
            );
106

            
107
            // Deposit the new supply dedicated to rewards in the pending rewards account
108
            if let Err(undistributed_rewards) =
109
10115
                T::Currency::resolve(&T::PendingRewardsAccount::get(), staking_rewards)
110
            {
111
                total_reminder = total_reminder.merge(undistributed_rewards);
112
10115
            }
113

            
114
            // Keep track of chains to reward
115
10115
            ChainsToReward::<T>::put(ChainsToRewardValue {
116
10115
                para_ids: registered_para_ids,
117
10115
                rewards_per_chain,
118
10115
            });
119
10115

            
120
10115
            // Let the runtime handle the non-staking part
121
10115
            T::OnUnbalanced::on_unbalanced(not_distributed_rewards.merge(total_reminder));
122
10115

            
123
10115
            weight += Self::reward_orchestrator_author();
124
10115

            
125
10115
            weight
126
10115
        }
127
    }
128

            
129
    #[pallet::config]
130
    pub trait Config: frame_system::Config {
131
        /// Overarching event type.
132
        type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
133

            
134
        type Currency: Inspect<Self::AccountId> + Balanced<Self::AccountId>;
135

            
136
        type ContainerChains: GetCurrentContainerChains;
137

            
138
        /// Get block author for self chain
139
        type GetSelfChainBlockAuthor: Get<Self::AccountId>;
140

            
141
        /// Inflation rate per orchestrator block (proportion of the total issuance)
142
        #[pallet::constant]
143
        type InflationRate: Get<Perbill>;
144

            
145
        /// What to do with the new supply not dedicated to staking
146
        type OnUnbalanced: OnUnbalanced<CreditOf<Self>>;
147

            
148
        /// The account that will store rewards waiting to be paid out
149
        #[pallet::constant]
150
        type PendingRewardsAccount: Get<Self::AccountId>;
151

            
152
        /// Staking rewards distribution implementation
153
        type StakingRewardsDistributor: DistributeRewards<Self::AccountId, CreditOf<Self>>;
154

            
155
        /// Proportion of the new supply dedicated to staking
156
        #[pallet::constant]
157
        type RewardsPortion: Get<Perbill>;
158
    }
159

            
160
    #[pallet::event]
161
16661
    #[pallet::generate_deposit(pub(super) fn deposit_event)]
162
    pub enum Event<T: Config> {
163
12
        /// Rewarding orchestrator author
164
        RewardedOrchestrator {
165
            account_id: T::AccountId,
166
            balance: BalanceOf<T>,
167
        },
168
        /// Rewarding container author
169
        RewardedContainer {
170
            account_id: T::AccountId,
171
            para_id: ParaId,
172
            balance: BalanceOf<T>,
173
        },
174
    }
175

            
176
    /// Container chains to reward per block
177
87008
    #[pallet::storage]
178
    #[pallet::getter(fn container_chains_to_reward)]
179
    pub(super) type ChainsToReward<T: Config> =
180
        StorageValue<_, ChainsToRewardValue<T>, OptionQuery>;
181
528
    #[derive(Clone, Encode, Decode, PartialEq, sp_core::RuntimeDebug, scale_info::TypeInfo)]
182
    #[scale_info(skip_type_params(T))]
183
    pub struct ChainsToRewardValue<T: Config> {
184
        pub para_ids: BoundedVec<
185
            ParaId,
186
            <T::ContainerChains as GetCurrentContainerChains>::MaxContainerChains,
187
        >,
188
        pub rewards_per_chain: BalanceOf<T>,
189
    }
190

            
191
    impl<T: Config> Pallet<T> {
192
10115
        fn reward_orchestrator_author() -> Weight {
193
10115
            let mut total_weight = T::DbWeight::get().reads(1);
194
10115
            let orchestrator_author = T::GetSelfChainBlockAuthor::get();
195

            
196
10115
            if let Some(chains_to_reward) = ChainsToReward::<T>::get() {
197
10115
                total_weight += T::DbWeight::get().reads(1);
198
10115
                match T::StakingRewardsDistributor::distribute_rewards(
199
10115
                    orchestrator_author.clone(),
200
10115
                    T::Currency::withdraw(
201
10115
                        &T::PendingRewardsAccount::get(),
202
10115
                        chains_to_reward.rewards_per_chain,
203
10115
                        Precision::BestEffort,
204
10115
                        Preservation::Expendable,
205
10115
                        Fortitude::Force,
206
10115
                    )
207
10115
                    .unwrap_or(CreditOf::<T>::zero()),
208
10115
                ) {
209
10082
                    Ok(frame_support::dispatch::PostDispatchInfo { actual_weight, .. }) => {
210
10082
                        Self::deposit_event(Event::RewardedOrchestrator {
211
10082
                            account_id: orchestrator_author,
212
10082
                            balance: chains_to_reward.rewards_per_chain,
213
10082
                        });
214

            
215
10082
                        if let Some(weight) = actual_weight {
216
10072
                            total_weight += weight
217
10
                        }
218
                    }
219
33
                    Err(e) => {
220
33
                        log::debug!("Fail to distribute rewards: {:?}", e)
221
                    }
222
                }
223
            } else {
224
                panic!("ChainsToReward not filled");
225
            }
226

            
227
10115
            total_weight
228
10115
        }
229
    }
230
}
231

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

            
278
6579
                total_weight += T::DbWeight::get().writes(1);
279
6579
                // Keep track of chains to reward
280
6579
                ChainsToReward::<T>::put(container_chains_to_reward);
281
1
            }
282
        }
283
6580
        total_weight
284
6580
    }
285
}