From b25ccb10dc4b1e2b4719a87be78147cd161188ef Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 20 Oct 2023 11:40:08 +0200 Subject: [PATCH] Tier params & config init solution --- pallets/dapp-staking-v3/src/lib.rs | 6 +- .../dapp-staking-v3/src/test/tests_types.rs | 48 ++++++ pallets/dapp-staking-v3/src/types.rs | 151 +++++++++++++++++- 3 files changed, 201 insertions(+), 4 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 01a9560a71..348a101071 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -1012,7 +1012,8 @@ pub mod pallet { Ok(()) } - /// TODO + // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. + /// TODO: docs #[pallet::call_index(11)] #[pallet::weight(Weight::zero())] pub fn claim_staker_rewards(origin: OriginFor) -> DispatchResult { @@ -1108,7 +1109,8 @@ pub mod pallet { Ok(()) } - /// TODO + // TODO: perhaps this should be changed to include smart contract from which rewards are being claimed. + /// TODO: documentation #[pallet::call_index(12)] #[pallet::weight(Weight::zero())] pub fn claim_bonus_reward(origin: OriginFor) -> DispatchResult { diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index f92cca8b8f..0eb4544dc1 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -17,6 +17,8 @@ // along with Astar. If not, see . use frame_support::assert_ok; +use sp_arithmetic::fixed_point::FixedU64; +use sp_runtime::Permill; use crate::test::mock::{Balance, *}; use crate::*; @@ -1514,3 +1516,49 @@ fn era_reward_span_fails_when_expected() { Err(EraRewardSpanError::NoCapacity) ); } + +#[test] +fn tier_slot_configuration_basic_tests() { + // TODO: this should be expanded & improved later + get_u32_type!(TiersNum, 4); + let params = TierSlotParameters:: { + reward_portion: BoundedVec::try_from(vec![ + Permill::from_percent(40), + Permill::from_percent(30), + Permill::from_percent(20), + Permill::from_percent(10), + ]) + .unwrap(), + slot_distribution: BoundedVec::try_from(vec![ + Permill::from_percent(10), + Permill::from_percent(20), + Permill::from_percent(30), + Permill::from_percent(40), + ]) + .unwrap(), + tier_thresholds: BoundedVec::try_from(vec![ + TierThreshold::DynamicTvlAmount { amount: 1000 }, + TierThreshold::DynamicTvlAmount { amount: 500 }, + TierThreshold::DynamicTvlAmount { amount: 100 }, + TierThreshold::FixedTvlAmount { amount: 50 }, + ]) + .unwrap(), + }; + assert!(params.is_valid(), "Example params must be valid!"); + + // Create a configuration with some values + let init_config = TierSlotConfiguration:: { + number_of_slots: 100, + slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), + reward_portion: params.reward_portion.clone(), + tier_thresholds: params.tier_thresholds.clone(), + }; + assert!(init_config.is_valid(), "Init config must be valid!"); + + // Create a new config, based on a new price + let new_price = FixedU64::from_rational(20, 100); // in production will be expressed in USD + let new_config = init_config.calculate_new(new_price, ¶ms); + assert!(new_config.is_valid()); + + // TODO: expand tests, add more sanity checks (e.g. tier 3 requirement should never be lower than tier 4, etc.) +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 8065b35c90..20a98e41e8 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -21,9 +21,10 @@ use frame_support::{pallet_prelude::*, BoundedVec}; use frame_system::pallet_prelude::*; use parity_scale_codec::{Decode, Encode}; +use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, Zero}, - Saturating, + traits::{AtLeast32BitUnsigned, UniqueSaturatedInto, Zero}, + FixedPointNumber, Permill, Saturating, }; use astar_primitives::Balance; @@ -1357,3 +1358,149 @@ where } } } + +/// Description of tier entry requirement. +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +pub enum TierThreshold { + /// Entry into tier is mandated by minimum amount of staked funds. + /// Value is fixed, and is not expected to change in between periods. + FixedTvlAmount { amount: Balance }, + /// Entry into tier is mandated by minimum amount of staked funds. + /// Value is expected to dynamically change in-between periods, depending on the system parameters. + DynamicTvlAmount { amount: Balance }, + // TODO: perhaps add a type that is dynamic but has lower bound below which the value cannot go? + // Otherwise we could allow e.g. tier 3 to go below tier 4, which doesn't make sense. +} + +/// Top level description of tier slot parameters used to calculate tier configuration. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(NT))] +pub struct TierSlotParameters> { + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub reward_portion: BoundedVec, + /// Distribution of number of slots per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub slot_distribution: BoundedVec, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec, +} + +impl> TierSlotParameters { + /// Check if configuration is valid. + /// All vectors are expected to have exactly the amount of entries as `number_of_tiers`. + pub fn is_valid(&self) -> bool { + let number_of_tiers: usize = NT::get() as usize; + number_of_tiers == self.reward_portion.len() + && number_of_tiers == self.slot_distribution.len() + && number_of_tiers == self.tier_thresholds.len() + } +} + +/// Configuration of dApp tiers. +#[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] +#[scale_info(skip_type_params(NT))] +pub struct TierSlotConfiguration> { + /// Total number of slots. + #[codec(compact)] + pub number_of_slots: u16, + /// Number of slots per tier. + /// First entry refers to the first tier, and so on. + pub slots_per_tier: BoundedVec, + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must be exactly equal to 1. + pub reward_portion: BoundedVec, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec, +} + +impl> TierSlotConfiguration { + /// Check if parameters are valid. + pub fn is_valid(&self) -> bool { + let number_of_tiers: usize = NT::get() as usize; + number_of_tiers == self.slots_per_tier.len() + // All vecs length must match number of tiers. + && number_of_tiers == self.reward_portion.len() + && number_of_tiers == self.tier_thresholds.len() + // Total number of slots must match the sum of slots per tier. + && self.slots_per_tier.iter().fold(0, |acc, x| acc + x) == self.number_of_slots + } + + /// TODO + pub fn calculate_new(&self, native_price: FixedU64, params: &TierSlotParameters) -> Self { + let new_number_of_slots = Self::calculate_number_of_slots(native_price); + + // TODO: ugly, unsafe, refactor later + let new_slots_per_tier: Vec = params + .slot_distribution + .clone() + .into_inner() + .iter() + .map(|x| *x * new_number_of_slots as u128) + .map(|x| x.unique_saturated_into()) + .collect(); + let new_slots_per_tier = + BoundedVec::::try_from(new_slots_per_tier).unwrap_or_default(); + + // TODO: document this, and definitely refactor it to be simpler. + let new_tier_thresholds = if new_number_of_slots > self.number_of_slots { + let delta_threshold_decrease = FixedU64::from_rational( + (new_number_of_slots - self.number_of_slots).into(), + new_number_of_slots.into(), + ); + + let mut new_tier_thresholds = self.tier_thresholds.clone(); + new_tier_thresholds + .iter_mut() + .for_each(|threshold| match threshold { + TierThreshold::DynamicTvlAmount { amount } => { + *amount = amount + .saturating_sub(delta_threshold_decrease.saturating_mul_int(*amount)); + } + _ => (), + }); + + new_tier_thresholds + } else if new_number_of_slots < self.number_of_slots { + let delta_threshold_increase = FixedU64::from_rational( + (self.number_of_slots - new_number_of_slots).into(), + new_number_of_slots.into(), + ); + + let mut new_tier_thresholds = self.tier_thresholds.clone(); + new_tier_thresholds + .iter_mut() + .for_each(|threshold| match threshold { + TierThreshold::DynamicTvlAmount { amount } => { + *amount = amount + .saturating_add(delta_threshold_increase.saturating_mul_int(*amount)); + } + _ => (), + }); + + new_tier_thresholds + } else { + self.tier_thresholds.clone() + }; + + Self { + number_of_slots: new_number_of_slots, + slots_per_tier: new_slots_per_tier, + reward_portion: params.reward_portion.clone(), + tier_thresholds: new_tier_thresholds, + } + } + + /// Calculate number of slots, based on the provided native token price. + pub fn calculate_number_of_slots(native_price: FixedU64) -> u16 { + // floor(1000 x price + 50) + let result: u64 = native_price.saturating_mul_int(1000).saturating_add(50); + + result.unique_saturated_into() + } +}