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::{oracle::CurrencyAmount, 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::{
27    traits::{UniqueSaturatedInto, Zero},
28    FixedPointNumber,
29};
30use sp_std::hash::Hash;
31
32/// Era number type
33pub type EraNumber = u32;
34/// Period number type
35pub type PeriodNumber = u32;
36/// Dapp Id type
37pub type DAppId = u16;
38/// Tier Id type
39pub type TierId = u8;
40// Tier Rank type
41pub type Rank = u8;
42
43/// Configuration for cycles, periods, subperiods & eras.
44///
45/// * `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.
46/// * `period` - Period consists of two distinct subperiods: `Voting` & `Build&Earn`. They are integral parts of dApp staking.
47///              Length is expressed in standard eras or just _eras_.
48/// * `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.
49///           Era length is expressed in blocks.
50pub trait CycleConfiguration {
51    /// How many different periods are there in a cycle (a 'year').
52    ///
53    /// This value has to be at least 1.
54    fn periods_per_cycle() -> PeriodNumber;
55
56    /// For how many standard era lengths does the voting subperiod last.
57    ///
58    /// This value has to be at least 1.
59    fn eras_per_voting_subperiod() -> EraNumber;
60
61    /// How many standard eras are there in the build&earn subperiod.
62    ///
63    /// This value has to be at least 1.
64    fn eras_per_build_and_earn_subperiod() -> EraNumber;
65
66    /// How many blocks are there per standard era.
67    ///
68    /// This value has to be at least 1.
69    fn blocks_per_era() -> BlockNumber;
70
71    /// For how many standard era lengths does the period last.
72    fn period_in_era_lengths() -> EraNumber {
73        Self::eras_per_voting_subperiod().saturating_add(Self::eras_per_build_and_earn_subperiod())
74    }
75
76    /// For how many standard era lengths does the cycle (a 'year') last.
77    fn cycle_in_era_lengths() -> EraNumber {
78        Self::period_in_era_lengths().saturating_mul(Self::periods_per_cycle())
79    }
80
81    /// How many blocks are there per cycle (a 'year').
82    fn blocks_per_cycle() -> BlockNumber {
83        Self::blocks_per_era().saturating_mul(Self::cycle_in_era_lengths())
84    }
85
86    /// For how many standard era lengths do all the build&earn subperiods in a cycle last.
87    fn build_and_earn_eras_per_cycle() -> EraNumber {
88        Self::eras_per_build_and_earn_subperiod().saturating_mul(Self::periods_per_cycle())
89    }
90
91    /// How many distinct eras are there in a single period.
92    fn eras_per_period() -> EraNumber {
93        Self::eras_per_build_and_earn_subperiod().saturating_add(1)
94    }
95
96    /// How many distinct eras are there in a cycle.
97    fn eras_per_cycle() -> EraNumber {
98        Self::eras_per_period().saturating_mul(Self::periods_per_cycle())
99    }
100}
101
102/// Trait for observers (listeners) of various events related to dApp staking protocol.
103pub trait Observer {
104    /// Called in the block right before the next era starts.
105    ///
106    /// Returns the weight consumed by the call.
107    ///
108    /// # Arguments
109    /// * `next_era` - Era number of the next era.
110    fn block_before_new_era(_next_era: EraNumber) -> Weight {
111        Weight::zero()
112    }
113}
114
115impl Observer for () {}
116
117/// Interface for staking reward handler.
118///
119/// Provides reward pool values for stakers - normal & bonus rewards, as well as dApp reward pool.
120/// Also provides a safe function for paying out rewards.
121pub trait StakingRewardHandler<AccountId> {
122    /// Returns the staker reward pool & dApp reward pool for an era.
123    ///
124    /// The total staker reward pool is dynamic and depends on the total value staked.
125    fn staker_and_dapp_reward_pools(total_value_staked: Balance) -> (Balance, Balance);
126
127    /// Returns the bonus reward pool for a period.
128    fn bonus_reward_pool() -> Balance;
129
130    /// Attempts to pay out the rewards to the beneficiary.
131    fn payout_reward(beneficiary: &AccountId, reward: Balance) -> Result<(), ()>;
132}
133
134/// Trait defining the interface for dApp staking `smart contract types` handler.
135///
136/// It can be used to create a representation of the specified smart contract instance type.
137pub trait SmartContractHandle<AccountId> {
138    /// Create a new smart contract representation for the specified EVM address.
139    fn evm(address: H160) -> Self;
140    /// Create a new smart contract representation for the specified Wasm address.
141    fn wasm(address: AccountId) -> Self;
142}
143
144/// Multi-VM pointer to smart contract instance.
145#[derive(
146    PartialEq,
147    Eq,
148    Copy,
149    Clone,
150    Encode,
151    Decode,
152    DecodeWithMemTracking,
153    RuntimeDebug,
154    MaxEncodedLen,
155    Hash,
156    scale_info::TypeInfo,
157)]
158pub enum SmartContract<AccountId> {
159    /// EVM smart contract instance.
160    Evm(H160),
161    /// Wasm smart contract instance.
162    Wasm(AccountId),
163}
164
165impl<AccountId> SmartContractHandle<AccountId> for SmartContract<AccountId> {
166    fn evm(address: H160) -> Self {
167        Self::Evm(address)
168    }
169
170    fn wasm(address: AccountId) -> Self {
171        Self::Wasm(address)
172    }
173}
174
175/// Used to check whether an account is allowed to participate in dApp staking or not.
176pub trait AccountCheck<AccountId> {
177    /// `true` if the account is allowed to stake, `false` otherwise.
178    fn allowed_to_stake(account: &AccountId) -> bool;
179}
180
181impl<AccountId> AccountCheck<AccountId> for () {
182    fn allowed_to_stake(_account: &AccountId) -> bool {
183        true
184    }
185}
186
187/// Trait for calculating the total number of tier slots for the given price.
188pub trait TierSlots {
189    /// Returns the total number of tier slots for the given price.
190    ///
191    /// # Arguments
192    /// * `price` - price (e.g. moving average over some time period) of the native currency.
193    /// * `args` - arguments, `a` & `b`, for the linear equation `number_of_slots = a * price + b`.
194    ///
195    /// Returns the total number of tier slots.
196    fn number_of_slots(price: CurrencyAmount, args: (u64, u64)) -> u16;
197}
198
199/// Standard tier slots implementation, as proposed in the Tokenomics 2.0 document.
200pub struct StandardTierSlots;
201impl TierSlots for StandardTierSlots {
202    fn number_of_slots(price: CurrencyAmount, args: (u64, u64)) -> u16 {
203        let result: u64 = price.saturating_mul_int(args.0).saturating_add(args.1);
204        result.unique_saturated_into()
205    }
206}
207
208/// Standard tier slots arguments.
209/// Initially decided for Astar, during the Tokenomics 2.0 work.
210pub const STANDARD_TIER_SLOTS_ARGS: (u64, u64) = (1000, 50);
211
212/// 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`.
213/// First 4 bits are used to hold rank and second 4 bits are used to hold tier.
214/// i.e: 0xa1 will hold rank: 10 and tier: 1 (0xa1 & 0xf == 1; 0xa1 >> 4 == 10;)
215#[derive(Copy, Clone, Encode, Decode, Eq, PartialEq, MaxEncodedLen, scale_info::TypeInfo)]
216pub struct RankedTier(u8);
217
218impl RankedTier {
219    pub const MAX_RANK: u8 = 10;
220
221    /// Create new encoded RankedTier from tier and rank.
222    /// Returns Err(ArithmeticError::Overflow) if max value is not respected.
223    pub fn new(tier: TierId, rank: Rank) -> Result<Self, ArithmeticError> {
224        if rank > Self::MAX_RANK || tier > 0xf {
225            return Err(ArithmeticError::Overflow);
226        }
227        Ok(Self(rank << 4 | tier & 0x0f))
228    }
229
230    /// Create new encoded RankedTier from tier and rank with saturation.
231    pub fn new_saturated(tier: TierId, rank: Rank) -> Self {
232        Self(rank.min(Self::MAX_RANK) << 4 | tier.min(0xf) & 0x0f)
233    }
234
235    #[inline(always)]
236    pub fn tier(&self) -> TierId {
237        self.0 & 0x0f
238    }
239
240    #[inline(always)]
241    pub fn rank(&self) -> Rank {
242        (self.0 >> 4).min(Self::MAX_RANK)
243    }
244
245    #[inline(always)]
246    pub fn deconstruct(&self) -> (TierId, Rank) {
247        (self.tier(), self.rank())
248    }
249}
250
251impl core::fmt::Debug for RankedTier {
252    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
253        f.debug_struct("RankedTier")
254            .field("tier", &self.tier())
255            .field("rank", &self.rank())
256            .finish()
257    }
258}
259
260impl RankedTier {
261    /// Find rank based on lower/upper bounds and staked amount.
262    /// Delta between upper and lower bound is divided in 10 and will increase rank
263    /// by one for each threshold staked amount will reach.
264    /// i.e. find_rank(10, 20, 10) -> 0
265    /// i.e. find_rank(10, 20, 15) -> 5
266    /// i.e. find_rank(10, 20, 20) -> 10
267    pub fn find_rank(lower_bound: Balance, upper_bound: Balance, stake_amount: Balance) -> Rank {
268        if upper_bound.is_zero() {
269            return 0;
270        }
271        let rank_threshold = upper_bound
272            .saturating_sub(lower_bound)
273            .saturating_div(RankedTier::MAX_RANK.into());
274        if rank_threshold.is_zero() {
275            0
276        } else {
277            <Balance as TryInto<u8>>::try_into(
278                stake_amount
279                    .saturating_sub(lower_bound)
280                    .saturating_div(rank_threshold),
281            )
282            .unwrap_or_default()
283            .min(RankedTier::MAX_RANK)
284        }
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[test]
293    fn tier_and_rank() {
294        let t = RankedTier::new(0, 0).unwrap();
295        assert_eq!(t.deconstruct(), (0, 0));
296
297        let t = RankedTier::new(15, 10).unwrap();
298        assert_eq!(t.deconstruct(), (15, 10));
299
300        assert_eq!(RankedTier::new(16, 10), Err(ArithmeticError::Overflow));
301        assert_eq!(RankedTier::new(15, 11), Err(ArithmeticError::Overflow));
302
303        let t = RankedTier::new_saturated(0, 0);
304        assert_eq!(t.deconstruct(), (0, 0));
305
306        let t = RankedTier::new_saturated(1, 1);
307        assert_eq!(t.deconstruct(), (1, 1));
308
309        let t = RankedTier::new_saturated(3, 15);
310        assert_eq!(t.deconstruct(), (3, 10));
311
312        // max value for tier and rank
313        let t = RankedTier::new_saturated(16, 16);
314        assert_eq!(t.deconstruct(), (15, 10));
315    }
316
317    #[test]
318    fn find_rank() {
319        assert_eq!(RankedTier::find_rank(0, 0, 0), 0);
320        assert_eq!(RankedTier::find_rank(0, 100, 9), 0);
321        assert_eq!(RankedTier::find_rank(0, 100, 10), 1);
322        assert_eq!(RankedTier::find_rank(0, 100, 49), 4);
323        assert_eq!(RankedTier::find_rank(0, 100, 50), 5);
324        assert_eq!(RankedTier::find_rank(0, 100, 51), 5);
325        assert_eq!(RankedTier::find_rank(0, 100, 101), 10);
326
327        assert_eq!(RankedTier::find_rank(100, 100, 100), 0);
328        assert_eq!(RankedTier::find_rank(200, 100, 100), 0);
329    }
330}