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}