astar_primitives/dapp_staking.rs
1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19use super::{Balance, BlockNumber};
20
21use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
22
23use frame_support::pallet_prelude::{RuntimeDebug, Weight};
24use sp_arithmetic::ArithmeticError;
25use sp_core::{DecodeWithMemTracking, H160};
26use sp_runtime::traits::Zero;
27use sp_std::hash::Hash;
28
29/// Era number type
30pub type EraNumber = u32;
31/// Period number type
32pub type PeriodNumber = u32;
33/// Dapp Id type
34pub type DAppId = u16;
35/// Tier Id type
36pub type TierId = u8;
37/// Tier Rank type
38pub type Rank = u8;
39
40/// Configuration for cycles, periods, subperiods & eras.
41///
42/// * `cycle` - Time unit similar to 'year' in the real world. Consists of one or more periods. At the beginning of each cycle, inflation is recalculated.
43/// * `period` - Period consists of two distinct subperiods: `Voting` & `Build&Earn`. They are integral parts of dApp staking.
44/// Length is expressed in standard eras or just _eras_.
45/// * `era` - Era is the basic time unit in the dApp staking protocol. At the end of each era, reward pools for stakers & dApps are calculated.
46/// Era length is expressed in blocks.
47pub trait CycleConfiguration {
48 /// How many different periods are there in a cycle (a 'year').
49 ///
50 /// This value has to be at least 1.
51 fn periods_per_cycle() -> PeriodNumber;
52
53 /// For how many standard era lengths does the voting subperiod last.
54 ///
55 /// This value has to be at least 1.
56 fn eras_per_voting_subperiod() -> EraNumber;
57
58 /// How many standard eras are there in the build&earn subperiod.
59 ///
60 /// This value has to be at least 1.
61 fn eras_per_build_and_earn_subperiod() -> EraNumber;
62
63 /// How many blocks are there per standard era.
64 ///
65 /// This value has to be at least 1.
66 fn blocks_per_era() -> BlockNumber;
67
68 /// For how many standard era lengths does the period last.
69 fn period_in_era_lengths() -> EraNumber {
70 Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod())
71 }
72
73 /// For how many standard era lengths does the cycle (a 'year') last.
74 fn cycle_in_era_lengths() -> EraNumber {
75 Self::period_in_era_lengths().saturating_mul(Self::periods_per_cycle())
76 }
77
78 /// How many blocks are there per cycle (a 'year').
79 fn blocks_per_cycle() -> BlockNumber {
80 Self::blocks_per_era().saturating_mul(Self::cycle_in_era_lengths())
81 }
82
83 /// For how many standard era lengths do all the build&earn subperiods in a cycle last.
84 fn build_and_earn_eras_per_cycle() -> EraNumber {
85 Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle())
86 }
87
88 /// How many distinct eras are there in a single period.
89 fn eras_per_period() -> EraNumber {
90 Self::eras_per_build_and_earn_subperiod().saturating_add(1)
91 }
92
93 /// How many distinct eras are there in a cycle.
94 fn eras_per_cycle() -> EraNumber {
95 Self::eras_per_period().saturating_mul(Self::periods_per_cycle())
96 }
97}
98
99/// Trait for observers (listeners) of various events related to dApp staking protocol.
100pub trait Observer {
101 /// Called in the block right before the next era starts.
102 ///
103 /// Returns the weight consumed by the call.
104 ///
105 /// # Arguments
106 /// * `next_era` - Era number of the next era.
107 fn block_before_new_era(_next_era: EraNumber) -> Weight {
108 Weight::zero()
109 }
110}
111
112impl Observer for () {}
113
114/// Interface for staking reward handler.
115///
116/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool.
117/// Also provides a safe function for paying out rewards.
118pub trait StakingRewardHandler<AccountId> {
119 /// Returns the staker reward pool & dApp reward pool for an era.
120 ///
121 /// The total staker reward pool is dynamic and depends on the total value staked.
122 fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance);
123
124 /// Returns the bonus reward pool for a period.
125 fn bonus_reward_pool() -> Balance;
126
127 /// Attempts to pay out the rewards to the beneficiary.
128 fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>;
129}
130
131/// Trait defining the interface for dApp staking `smart contract types` handler.
132///
133/// It can be used to create a representation of the specified smart contract instance type.
134pub trait SmartContractHandle<AccountId> {
135 /// Create a new smart contract representation for the specified EVM address.
136 fn evm(address: H160) -> Self;
137 /// Create a new smart contract representation for the specified Wasm address.
138 ///
139 /// # Deprecated
140 ///
141 /// Wasm (ink!) smart contracts have been decommissioned together with `pallet-contracts`.
142 /// No new Wasm dApp can be registered; this constructor is only kept so that historic
143 /// storage entries remain decodable. It will be removed once dApp staking storage on all
144 /// networks is provably free of `SmartContract::Wasm` entries.
145 fn wasm(address: AccountId) -> Self;
146}
147
148/// Multi-VM pointer to smart contract instance.
149#[derive(
150 PartialEq,
151 Eq,
152 Copy,
153 Clone,
154 Encode,
155 Decode,
156 DecodeWithMemTracking,
157 RuntimeDebug,
158 MaxEncodedLen,
159 Hash,
160 scale_info::TypeInfo,
161)]
162pub enum SmartContract<AccountId> {
163 /// EVM smart contract instance.
164 Evm(H160),
165 /// Wasm smart contract instance.
166 ///
167 /// # Deprecated
168 ///
169 /// `pallet-contracts` has been removed from all Astar networks and ink! is discontinued,
170 /// so no new dApp can be registered under this variant.
171 ///
172 /// The variant itself MUST NOT be removed until dApp staking storage (`IntegratedDApps`,
173 /// `StakerInfo`) is provably free of Wasm-keyed entries on Astar, Shiden and Shibuya --
174 /// dropping it earlier would make those entries undecodable and brick the pallet.
175 Wasm(AccountId),
176}
177
178impl<AccountId> SmartContractHandle<AccountId> for SmartContract<AccountId> {
179 fn evm(address: H160) -> Self {
180 Self::Evm(address)
181 }
182
183 fn wasm(address: AccountId) -> Self {
184 Self::Wasm(address)
185 }
186}
187
188/// Used to check whether an account is allowed to participate in dApp staking or not.
189pub trait AccountCheck<AccountId> {
190 /// `true` if the account is allowed to stake, `false` otherwise.
191 fn allowed_to_stake(account: &AccountId) -> bool;
192}
193
194impl<AccountId> AccountCheck<AccountId> for () {
195 fn allowed_to_stake(_account: &AccountId) -> bool {
196 true
197 }
198}
199
200/// Fixed number of tier slots used by dApp-staking recalculation.
201pub const FIXED_NUMBER_OF_TIER_SLOTS: u16 = 16;
202
203/// RankedTier is wrapper around u8 to hold both tier and rank. u8 has 2 bytes (8bits) and they're using in this order `0xrank_tier`.
204/// First 4 bits are used to hold rank and second 4 bits are used to hold tier.
205/// i.e: 0xa1 will hold rank: 10 and tier: 1 (0xa1 & 0xf == 1; 0xa1 >> 4 == 10;)
206#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, MaxEncodedLen, scale_info::TypeInfo)]
207pub struct RankedTier(u8);
208
209impl RankedTier {
210 pub const MAX_RANK: u8 = 10;
211
212 /// Create new encoded RankedTier from tier and rank.
213 /// Returns Err(ArithmeticError::Overflow) if max value is not respected.
214 pub fn new(tier: TierId, rank: Rank) -> Result<Self, ArithmeticError> {
215 if rank > Self::MAX_RANK || tier > 0xf {
216 return Err(ArithmeticError::Overflow);
217 }
218 Ok(Self(rank << 4 | tier & 0x0f))
219 }
220
221 /// Create new encoded RankedTier from tier and rank with saturation.
222 pub fn new_saturated(tier: TierId, rank: Rank) -> Self {
223 Self(rank.min(Self::MAX_RANK) << 4 | tier.min(0xf) & 0x0f)
224 }
225
226 #[inline(always)]
227 pub fn tier(&self) -> TierId {
228 self.0 & 0x0f
229 }
230
231 #[inline(always)]
232 pub fn rank(&self) -> Rank {
233 (self.0 >> 4).min(Self::MAX_RANK)
234 }
235
236 #[inline(always)]
237 pub fn deconstruct(&self) -> (TierId, Rank) {
238 (self.tier(), self.rank())
239 }
240}
241
242impl core::fmt::Debug for RankedTier {
243 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244 f.debug_struct("RankedTier")
245 .field("tier", &self.tier())
246 .field("rank", &self.rank())
247 .finish()
248 }
249}
250
251impl RankedTier {
252 /// Find rank based on lower/upper bounds and staked amount.
253 /// Delta between upper and lower bound is divided in 10 and will increase rank
254 /// by one for each threshold staked amount will reach.
255 /// i.e. find_rank(10, 20, 10) -> 0
256 /// i.e. find_rank(10, 20, 15) -> 5
257 /// i.e. find_rank(10, 20, 20) -> 10
258 pub fn find_rank(lower_bound: Balance, upper_bound: Balance, stake_amount: Balance) -> Rank {
259 if upper_bound.is_zero() {
260 return 0;
261 }
262 let rank_threshold = upper_bound
263 .saturating_sub(lower_bound)
264 .saturating_div(RankedTier::MAX_RANK.into());
265 if rank_threshold.is_zero() {
266 0
267 } else {
268 <Balance as TryInto<u8>>::try_into(
269 stake_amount
270 .saturating_sub(lower_bound)
271 .saturating_div(rank_threshold),
272 )
273 .unwrap_or_default()
274 .min(RankedTier::MAX_RANK)
275 }
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn tier_and_rank() {
285 let t = RankedTier::new(0, 0).unwrap();
286 assert_eq!(t.deconstruct(), (0, 0));
287
288 let t = RankedTier::new(15, 10).unwrap();
289 assert_eq!(t.deconstruct(), (15, 10));
290
291 assert_eq!(RankedTier::new(16, 10), Err(ArithmeticError::Overflow));
292 assert_eq!(RankedTier::new(15, 11), Err(ArithmeticError::Overflow));
293
294 let t = RankedTier::new_saturated(0, 0);
295 assert_eq!(t.deconstruct(), (0, 0));
296
297 let t = RankedTier::new_saturated(1, 1);
298 assert_eq!(t.deconstruct(), (1, 1));
299
300 let t = RankedTier::new_saturated(3, 15);
301 assert_eq!(t.deconstruct(), (3, 10));
302
303 // max value for tier and rank
304 let t = RankedTier::new_saturated(16, 16);
305 assert_eq!(t.deconstruct(), (15, 10));
306 }
307
308 #[test]
309 fn find_rank() {
310 assert_eq!(RankedTier::find_rank(0, 0, 0), 0);
311 assert_eq!(RankedTier::find_rank(0, 100, 9), 0);
312 assert_eq!(RankedTier::find_rank(0, 100, 10), 1);
313 assert_eq!(RankedTier::find_rank(0, 100, 49), 4);
314 assert_eq!(RankedTier::find_rank(0, 100, 50), 5);
315 assert_eq!(RankedTier::find_rank(0, 100, 51), 5);
316 assert_eq!(RankedTier::find_rank(0, 100, 101), 10);
317
318 assert_eq!(RankedTier::find_rank(100, 100, 100), 0);
319 assert_eq!(RankedTier::find_rank(200, 100, 100), 0);
320 }
321}