diff --git a/bin/collator/src/local/chain_spec.rs b/bin/collator/src/local/chain_spec.rs index c368889bf..21bfd479b 100644 --- a/bin/collator/src/local/chain_spec.rs +++ b/bin/collator/src/local/chain_spec.rs @@ -29,7 +29,7 @@ use sc_service::ChainType; use sp_core::{crypto::Ss58Codec, sr25519, Pair, Public}; use sp_runtime::{ traits::{AccountIdConversion, IdentifyAccount, Verify}, - Permill, + Perbill, Permill, }; type AccountPublic = <Signature as Verify>::Signer; @@ -163,19 +163,21 @@ fn testnet_genesis( Permill::from_percent(40), ], tier_thresholds: vec![ - TierThreshold::DynamicTvlAmount { - amount: 100 * AST, - minimum_amount: 80 * AST, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(35_700_000), // 3.57% + minimum_required_percentage: Perbill::from_parts(23_800_000), // 2.38% }, - TierThreshold::DynamicTvlAmount { - amount: 50 * AST, - minimum_amount: 40 * AST, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(8_900_000), // 0.89% + minimum_required_percentage: Perbill::from_parts(6_000_000), // 0.6% }, - TierThreshold::DynamicTvlAmount { - amount: 20 * AST, - minimum_amount: 20 * AST, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(23_800_000), // 2.38% + minimum_required_percentage: Perbill::from_parts(17_900_000), // 1.79% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(600_000), // 0.06% }, - TierThreshold::FixedTvlAmount { amount: 10 * AST }, ], slots_per_tier: vec![10, 20, 30, 40], safeguard: Some(false), diff --git a/bin/collator/src/parachain/chain_spec/astar.rs b/bin/collator/src/parachain/chain_spec/astar.rs index 4db49a20c..bd2766059 100644 --- a/bin/collator/src/parachain/chain_spec/astar.rs +++ b/bin/collator/src/parachain/chain_spec/astar.rs @@ -30,7 +30,7 @@ use sc_service::ChainType; use sp_core::{sr25519, Pair, Public}; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Permill, + Perbill, Permill, }; const PARA_ID: u32 = 2006; @@ -167,21 +167,22 @@ fn make_genesis( Permill::from_percent(30), Permill::from_percent(40), ], + // percentages below are calulated based on total issuance at the time when dApp staking v3 was launched (8.4B) tier_thresholds: vec![ - TierThreshold::DynamicTvlAmount { - amount: 30000 * ASTR, - minimum_amount: 20000 * ASTR, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(35_700_000), // 3.57% + minimum_required_percentage: Perbill::from_parts(23_800_000), // 2.38% }, - TierThreshold::DynamicTvlAmount { - amount: 7500 * ASTR, - minimum_amount: 5000 * ASTR, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(8_900_000), // 0.89% + minimum_required_percentage: Perbill::from_parts(6_000_000), // 0.6% }, - TierThreshold::DynamicTvlAmount { - amount: 20000 * ASTR, - minimum_amount: 15000 * ASTR, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(2_380_000), // 0.238% + minimum_required_percentage: Perbill::from_parts(1_790_000), // 0.179% }, - TierThreshold::FixedTvlAmount { - amount: 5000 * ASTR, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(200_000), // 0.02% }, ], slots_per_tier: vec![10, 20, 30, 40], diff --git a/bin/collator/src/parachain/chain_spec/shibuya.rs b/bin/collator/src/parachain/chain_spec/shibuya.rs index f8632c264..0a2d52d4f 100644 --- a/bin/collator/src/parachain/chain_spec/shibuya.rs +++ b/bin/collator/src/parachain/chain_spec/shibuya.rs @@ -33,7 +33,7 @@ use sp_core::{sr25519, Pair, Public}; use astar_primitives::oracle::CurrencyAmount; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Permill, + Perbill, Permill, }; use super::{get_from_seed, Extensions}; @@ -179,20 +179,23 @@ fn make_genesis( Permill::from_percent(30), Permill::from_percent(40), ], + // percentages below are calulated based on a total issuance at the time when dApp staking v3 was launched (147M) tier_thresholds: vec![ - TierThreshold::DynamicTvlAmount { - amount: 100 * SBY, - minimum_amount: 80 * SBY, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(20_000), // 0.0020% + minimum_required_percentage: Perbill::from_parts(17_000), // 0.0017% }, - TierThreshold::DynamicTvlAmount { - amount: 50 * SBY, - minimum_amount: 40 * SBY, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(13_000), // 0.0013% + minimum_required_percentage: Perbill::from_parts(10_000), // 0.0010% }, - TierThreshold::DynamicTvlAmount { - amount: 20 * SBY, - minimum_amount: 20 * SBY, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(5_400), // 0.00054% + minimum_required_percentage: Perbill::from_parts(3_400), // 0.00034% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(1_400), // 0.00014% }, - TierThreshold::FixedTvlAmount { amount: 10 * SBY }, ], slots_per_tier: vec![10, 20, 30, 40], safeguard: Some(false), diff --git a/bin/collator/src/parachain/chain_spec/shiden.rs b/bin/collator/src/parachain/chain_spec/shiden.rs index 67f15eb91..17fe56e8b 100644 --- a/bin/collator/src/parachain/chain_spec/shiden.rs +++ b/bin/collator/src/parachain/chain_spec/shiden.rs @@ -30,7 +30,7 @@ use sp_core::{sr25519, Pair, Public}; use astar_primitives::oracle::CurrencyAmount; use sp_runtime::{ traits::{IdentifyAccount, Verify}, - Permill, + Perbill, Permill, }; use super::{get_from_seed, Extensions}; @@ -169,20 +169,23 @@ fn make_genesis( Permill::from_percent(30), Permill::from_percent(40), ], + // percentages below are calulated based on a total issuance at the time when dApp staking v3 was launched (84.3M) tier_thresholds: vec![ - TierThreshold::DynamicTvlAmount { - amount: 30000 * SDN, - minimum_amount: 20000 * SDN, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(35_700_000), // 3.57% + minimum_required_percentage: Perbill::from_parts(23_800_000), // 2.38% }, - TierThreshold::DynamicTvlAmount { - amount: 7500 * SDN, - minimum_amount: 5000 * SDN, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(8_900_000), // 0.89% + minimum_required_percentage: Perbill::from_parts(6_000_000), // 0.6% }, - TierThreshold::DynamicTvlAmount { - amount: 20000 * SDN, - minimum_amount: 15000 * SDN, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(2_380_000), // 0.238% + minimum_required_percentage: Perbill::from_parts(1_790_000), // 0.179% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(600_000), // 0.06% }, - TierThreshold::FixedTvlAmount { amount: 5000 * SDN }, ], slots_per_tier: vec![10, 20, 30, 40], safeguard: Some(false), diff --git a/pallets/dapp-staking-v3/README.md b/pallets/dapp-staking-v3/README.md index 48a5b531a..b78827acd 100644 --- a/pallets/dapp-staking-v3/README.md +++ b/pallets/dapp-staking-v3/README.md @@ -208,7 +208,7 @@ At the end of each build&earn subperiod era, dApps are evaluated using a simple Based on this metric, they are sorted, and assigned to tiers. There is a limited number of tiers, and each tier has a limited capacity of slots. -Each tier also has a _threshold_ which a dApp must satisfy in order to enter it. +Each tier also has a _threshold_ which a dApp must satisfy in order to enter it. Thresholds for each tier are dynamically calculated as percentages of the total issuance at the time of the dApp staking v3 launch. Better tiers bring bigger rewards, so dApps are encouraged to compete for higher tiers and attract staker's support. For each tier, the reward pool and capacity are fixed. Each dApp within a tier always gets the same amount of reward. diff --git a/pallets/dapp-staking-v3/src/benchmarking/mod.rs b/pallets/dapp-staking-v3/src/benchmarking/mod.rs index 939b96369..bf0adfa0e 100644 --- a/pallets/dapp-staking-v3/src/benchmarking/mod.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/mod.rs @@ -704,10 +704,9 @@ mod benchmarks { // This is a hacky part to ensure we accommodate max number of contracts. TierConfig::<T>::mutate(|config| { let max_number_of_contracts: u16 = T::MaxNumberOfContracts::get().try_into().unwrap(); - config.number_of_slots = max_number_of_contracts; config.slots_per_tier[0] = max_number_of_contracts; config.slots_per_tier[1..].iter_mut().for_each(|x| *x = 0); - config.tier_thresholds[0] = TierThreshold::FixedTvlAmount { amount: 1 }; + config.tier_thresholds[0] = 1; }); force_advance_to_next_era::<T>(); let claim_era = ActiveProtocolState::<T>::get().era - 1; diff --git a/pallets/dapp-staking-v3/src/benchmarking/utils.rs b/pallets/dapp-staking-v3/src/benchmarking/utils.rs index a21123422..5e9ee0fb2 100644 --- a/pallets/dapp-staking-v3/src/benchmarking/utils.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/utils.rs @@ -177,32 +177,40 @@ pub(super) fn init_tier_settings<T: Config>() { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { - amount: 100 * UNIT, - minimum_amount: 80 * UNIT, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(11_112_000), // 1.1112% + minimum_required_percentage: Perbill::from_parts(8_889_000), // 0.8889% }, - TierThreshold::DynamicTvlAmount { - amount: 50 * UNIT, - minimum_amount: 40 * UNIT, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(5_556_000), // 0.5556% + minimum_required_percentage: Perbill::from_parts(4_400_000), // 0.44% }, - TierThreshold::DynamicTvlAmount { - amount: 20 * UNIT, - minimum_amount: 20 * UNIT, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(2_223_000), // 0.2223% + minimum_required_percentage: Perbill::from_parts(2_223_000), // 0.2223% }, - TierThreshold::FixedTvlAmount { - amount: MIN_TIER_THRESHOLD, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(1_667_000), // 0.1667% }, ]) .unwrap(), }; + let total_issuance = 1000 * MIN_TIER_THRESHOLD; + let tier_thresholds = tier_params + .tier_thresholds + .iter() + .map(|t| t.threshold(total_issuance)) + .collect::<Vec<Balance>>() + .try_into() + .expect("Invalid number of tier thresholds provided."); + // Init tier config, based on the initial params let init_tier_config = TiersConfiguration::<T::NumberOfTiers, T::TierSlots, T::BaseNativeCurrencyPrice> { - number_of_slots: NUMBER_OF_SLOTS.try_into().unwrap(), slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), reward_portion: tier_params.reward_portion.clone(), - tier_thresholds: tier_params.tier_thresholds.clone(), + tier_thresholds, _phantom: Default::default(), }; diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 324cba6b0..6580e3d08 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -94,7 +94,7 @@ pub mod pallet { use super::*; /// The current storage version. - pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(7); + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(8); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] @@ -520,8 +520,9 @@ pub mod pallet { reward_portion: vec![Permill::from_percent(100 / num_tiers); num_tiers as usize], slot_distribution: vec![Permill::from_percent(100 / num_tiers); num_tiers as usize], tier_thresholds: (0..num_tiers) - .map(|i| TierThreshold::FixedTvlAmount { - amount: (10 * i).into(), + .rev() + .map(|i| TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(i), }) .collect(), slots_per_tier: vec![100; num_tiers as usize], @@ -554,23 +555,27 @@ pub mod pallet { "Invalid tier parameters values provided." ); - // Prepare tier configuration and verify its correctness - let number_of_slots = self.slots_per_tier.iter().fold(0_u16, |acc, &slots| { - acc.checked_add(slots).expect("Overflow") - }); + let total_issuance = T::Currency::total_issuance(); + let tier_thresholds = tier_params + .tier_thresholds + .iter() + .map(|t| t.threshold(total_issuance)) + .collect::<Vec<Balance>>() + .try_into() + .expect("Invalid number of tier thresholds provided."); + let tier_config = TiersConfiguration::<T::NumberOfTiers, T::TierSlots, T::BaseNativeCurrencyPrice> { - number_of_slots, slots_per_tier: BoundedVec::<u16, T::NumberOfTiers>::try_from( self.slots_per_tier.clone(), ) .expect("Invalid number of slots per tier entries provided."), reward_portion: tier_params.reward_portion.clone(), - tier_thresholds: tier_params.tier_thresholds.clone(), + tier_thresholds, _phantom: Default::default(), }; assert!( - tier_params.is_valid(), + tier_config.is_valid(), "Invalid tier config values provided." ); @@ -1717,25 +1722,23 @@ pub mod pallet { let mut upper_bound = Balance::zero(); let mut rank_rewards = Vec::new(); - for (tier_id, (tier_capacity, tier_threshold)) in tier_config + for (tier_id, (tier_capacity, lower_bound)) in tier_config .slots_per_tier .iter() .zip(tier_config.tier_thresholds.iter()) .enumerate() { - let lower_bound = tier_threshold.threshold(); - // Iterate over dApps until one of two conditions has been met: // 1. Tier has no more capacity // 2. dApp doesn't satisfy the tier threshold (since they're sorted, none of the following dApps will satisfy the condition either) for (dapp_id, staked_amount) in dapp_stakes .iter() .skip(dapp_tiers.len()) - .take_while(|(_, amount)| tier_threshold.is_satisfied(*amount)) + .take_while(|(_, amount)| amount.ge(lower_bound)) .take(*tier_capacity as usize) { let rank = if T::RankingEnabled::get() { - RankedTier::find_rank(lower_bound, upper_bound, *staked_amount) + RankedTier::find_rank(*lower_bound, upper_bound, *staked_amount) } else { 0 }; @@ -1765,7 +1768,7 @@ pub mod pallet { rank_rewards.push(reward_per_rank); dapp_tiers.append(&mut tier_slots); - upper_bound = lower_bound; // current threshold becomes upper bound for next tier + upper_bound = *lower_bound; // current threshold becomes upper bound for next tier } // 5. @@ -1954,8 +1957,21 @@ pub mod pallet { // Re-calculate tier configuration for the upcoming new era let tier_params = StaticTierParams::<T>::get(); let average_price = T::NativePriceProvider::average_price(); - let new_tier_config = TierConfig::<T>::get().calculate_new(average_price, &tier_params); - TierConfig::<T>::put(new_tier_config); + let total_issuance = T::Currency::total_issuance(); + + let new_tier_config = + TierConfig::<T>::get().calculate_new(&tier_params, average_price, total_issuance); + + // Validate new tier configuration + if new_tier_config.is_valid() { + TierConfig::<T>::put(new_tier_config); + } else { + log::warn!( + target: LOG_TARGET, + "New tier configuration is invalid for era {}, preserving old one.", + next_era + ); + } Self::deposit_event(Event::<T>::NewEra { era: next_era }); if let Some(period_event) = maybe_period_event { diff --git a/pallets/dapp-staking-v3/src/migration.rs b/pallets/dapp-staking-v3/src/migration.rs index 287cd9b78..6918d848f 100644 --- a/pallets/dapp-staking-v3/src/migration.rs +++ b/pallets/dapp-staking-v3/src/migration.rs @@ -17,7 +17,10 @@ // along with Astar. If not, see <http://www.gnu.org/licenses/>. use super::*; -use frame_support::traits::OnRuntimeUpgrade; +use frame_support::{ + storage_alias, + traits::{GetStorageVersion, OnRuntimeUpgrade}, +}; #[cfg(feature = "try-runtime")] use sp_std::vec::Vec; @@ -38,12 +41,234 @@ pub mod versioned_migrations { Pallet<T>, <T as frame_system::Config>::DbWeight, >; + + /// Migration V7 to V8 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring + /// the migration is only performed when on-chain version is 7. + pub type V7ToV8<T, TierThresholds> = frame_support::migrations::VersionedMigration< + 7, + 8, + v8::VersionMigrateV7ToV8<T, TierThresholds>, + Pallet<T>, + <T as frame_system::Config>::DbWeight, + >; +} + +// TierThreshold as percentage of the total issuance +mod v8 { + use super::*; + use crate::migration::v7::TierParameters as TierParametersV7; + use crate::migration::v7::TiersConfiguration as TiersConfigurationV7; + + pub struct VersionMigrateV7ToV8<T, TierThresholds>(PhantomData<(T, TierThresholds)>); + + impl<T: Config, TierThresholds: Get<[TierThreshold; 4]>> OnRuntimeUpgrade + for VersionMigrateV7ToV8<T, TierThresholds> + { + fn on_runtime_upgrade() -> Weight { + // 1. Update static tier parameters with new thresholds from the runtime configurable param TierThresholds + let result = StaticTierParams::<T>::translate::<TierParametersV7<T::NumberOfTiers>, _>( + |maybe_old_params| match maybe_old_params { + Some(old_params) => { + let tier_thresholds: Result< + BoundedVec<TierThreshold, T::NumberOfTiers>, + _, + > = BoundedVec::try_from(TierThresholds::get().to_vec()); + + match tier_thresholds { + Ok(tier_thresholds) => Some(TierParameters { + slot_distribution: old_params.slot_distribution, + reward_portion: old_params.reward_portion, + tier_thresholds, + }), + Err(err) => { + log::error!( + "Failed to convert TierThresholds parameters: {:?}", + err + ); + None + } + } + } + _ => None, + }, + ); + + if result.is_err() { + log::error!("Failed to translate StaticTierParams from previous V7 type to current V8 type. Check TierParametersV7 decoding."); + // Enable maintenance mode. + ActiveProtocolState::<T>::mutate(|state| { + state.maintenance = true; + }); + log::warn!("Maintenance mode enabled."); + return T::DbWeight::get().reads_writes(1, 0); + } + + // 2. Translate tier thresholds from V7 TierThresholds to Balance + let result = TierConfig::<T>::translate::< + TiersConfigurationV7<T::NumberOfTiers, T::TierSlots, T::BaseNativeCurrencyPrice>, + _, + >(|maybe_old_config| match maybe_old_config { + Some(old_config) => { + let new_tier_thresholds: Result<BoundedVec<Balance, T::NumberOfTiers>, _> = + old_config + .tier_thresholds + .iter() + .map(|t| match t { + v7::TierThreshold::DynamicTvlAmount { amount, .. } => *amount, + v7::TierThreshold::FixedTvlAmount { amount } => *amount, + }) + .collect::<Vec<Balance>>() + .try_into(); + + match new_tier_thresholds { + Ok(new_tier_thresholds) => Some(TiersConfiguration { + slots_per_tier: old_config.slots_per_tier, + reward_portion: old_config.reward_portion, + tier_thresholds: new_tier_thresholds, + _phantom: Default::default(), + }), + Err(err) => { + log::error!("Failed to convert tier thresholds to balances: {:?}", err); + None + } + } + } + _ => None, + }); + + if result.is_err() { + log::error!("Failed to translate TierConfig from previous V7 type to current V8 type. Check TiersConfigurationV7 decoding."); + // Enable maintenance mode. + ActiveProtocolState::<T>::mutate(|state| { + state.maintenance = true; + }); + log::warn!("Maintenance mode enabled."); + return T::DbWeight::get().reads_writes(2, 1); + } + + T::DbWeight::get().reads_writes(2, 2) + } + + #[cfg(feature = "try-runtime")] + fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> { + let old_config = v7::TierConfig::<T>::get().ok_or_else(|| { + TryRuntimeError::Other( + "dapp-staking-v3::migration::v8: No old configuration found for TierConfig", + ) + })?; + Ok(old_config.number_of_slots.encode()) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> { + let old_number_of_slots = u16::decode(&mut &data[..]).map_err(|_| { + TryRuntimeError::Other("dapp-staking-v3::migration::v8: Failed to decode old value for number of slots") + })?; + + let actual_config = TierConfig::<T>::get(); + + // Calculated based on "slots_per_tier", which might have slight variations due to the nature of saturating permill distribution. + let actual_number_of_slots = actual_config.total_number_of_slots(); + let within_tolerance = (old_number_of_slots - 1)..=old_number_of_slots; + assert!( + within_tolerance.contains(&actual_number_of_slots), + "dapp-staking-v3::migration::v8: New TiersConfiguration format not set correctly, number of slots has derived. Old: {}. Actual: {}.", + old_number_of_slots, + actual_number_of_slots + ); + + assert!(actual_config.is_valid()); + + let actual_tier_params = StaticTierParams::<T>::get(); + assert!(actual_tier_params.is_valid()); + + let expected_tier_thresholds: BoundedVec<TierThreshold, T::NumberOfTiers> = + BoundedVec::try_from(TierThresholds::get().to_vec()).unwrap(); + let actual_tier_thresholds = actual_tier_params.tier_thresholds; + assert_eq!(expected_tier_thresholds, actual_tier_thresholds); + + ensure!( + Pallet::<T>::on_chain_storage_version() >= 8, + "dapp-staking-v3::migration::v8: Wrong storage version." + ); + Ok(()) + } + } } /// Translate DAppTiers to include rank rewards. mod v7 { use super::*; use crate::migration::v6::DAppTierRewards as DAppTierRewardsV6; + use astar_primitives::dapp_staking::TierSlots as TierSlotsFunc; + + /// Description of tier entry requirement. + #[derive(Encode, Decode)] + pub enum TierThreshold { + FixedTvlAmount { + amount: Balance, + }, + DynamicTvlAmount { + amount: Balance, + minimum_amount: Balance, + }, + } + + /// Top level description of tier slot parameters used to calculate tier configuration. + #[derive(Encode, Decode)] + pub struct TierParameters<NT: Get<u32>> { + /// Reward distribution per tier, in percentage. + /// First entry refers to the first tier, and so on. + /// The sum of all values must not exceed 100%. + /// In case it is less, portion of rewards will never be distributed. + pub reward_portion: BoundedVec<Permill, NT>, + /// 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 not exceed 100%. + /// In case it is less, slot capacity will never be fully filled. + pub slot_distribution: BoundedVec<Permill, NT>, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec<v7::TierThreshold, NT>, + } + + /// v7 type for configuration of dApp tiers. + #[derive(Encode, Decode)] + pub struct TiersConfiguration<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> { + /// 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<u16, NT>, + /// 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<Permill, NT>, + /// Requirements for entry into each tier. + /// First entry refers to the first tier, and so on. + pub tier_thresholds: BoundedVec<v7::TierThreshold, NT>, + /// Phantom data to keep track of the tier slots function. + #[codec(skip)] + pub(crate) _phantom: PhantomData<(T, P)>, + } + + /// v7 type for [`crate::StaticTierParams`] + #[storage_alias] + pub type StaticTierParams<T: Config> = + StorageValue<Pallet<T>, TierParameters<<T as Config>::NumberOfTiers>, ValueQuery>; + + /// v7 type for [`crate::TierConfig`] + #[storage_alias] + pub type TierConfig<T: Config> = StorageValue< + Pallet<T>, + TiersConfiguration< + <T as Config>::NumberOfTiers, + <T as Config>::TierSlots, + <T as Config>::BaseNativeCurrencyPrice, + >, + OptionQuery, + >; pub struct VersionMigrateV6ToV7<T>(PhantomData<T>); diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 2c4c502c6..dfebbdef0 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -318,36 +318,50 @@ impl ExtBuilder { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { - amount: 100, - minimum_amount: 80, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(11_112_000), // 1.1112% + minimum_required_percentage: Perbill::from_parts(8_889_000), // 0.8889% }, - TierThreshold::DynamicTvlAmount { - amount: 50, - minimum_amount: 40, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(5_556_000), // 0.5556% + minimum_required_percentage: Perbill::from_parts(4_400_000), // 0.44% }, - TierThreshold::DynamicTvlAmount { - amount: 20, - minimum_amount: 20, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(2_223_000), // 0.2223% + minimum_required_percentage: Perbill::from_parts(2_223_000), // 0.2223% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(1_667_000), // 0.1667% }, - TierThreshold::FixedTvlAmount { amount: 15 }, ]) .unwrap(), }; + let total_issuance = <Test as Config>::Currency::total_issuance(); + let tier_thresholds = tier_params + .tier_thresholds + .iter() + .map(|t| t.threshold(total_issuance)) + .collect::<Vec<Balance>>() + .try_into() + .expect("Invalid number of tier thresholds provided."); + // Init tier config, based on the initial params. Needs to be adjusted to the init price. let init_tier_config = TiersConfiguration::< <Test as Config>::NumberOfTiers, <Test as Config>::TierSlots, <Test as Config>::BaseNativeCurrencyPrice, > { - number_of_slots: 40, slots_per_tier: BoundedVec::try_from(vec![2, 5, 13, 20]).unwrap(), reward_portion: tier_params.reward_portion.clone(), - tier_thresholds: tier_params.tier_thresholds.clone(), + tier_thresholds, _phantom: Default::default(), } - .calculate_new(NATIVE_PRICE.with(|v| v.borrow().clone()), &tier_params); + .calculate_new( + &tier_params, + NATIVE_PRICE.with(|v| v.borrow().clone()), + total_issuance, + ); pallet_dapp_staking::StaticTierParams::<Test>::put(tier_params); pallet_dapp_staking::TierConfig::<Test>::put(init_tier_config.clone()); diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 5beebf351..d8f124683 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -20,7 +20,8 @@ use crate::test::{mock::*, testing_utils::*}; use crate::{ pallet::Config, ActiveProtocolState, ContractStake, DAppId, DAppTierRewardsFor, DAppTiers, EraRewards, Error, Event, ForcingType, GenesisConfig, IntegratedDApps, Ledger, NextDAppId, - PeriodNumber, Permill, Safeguard, StakerInfo, Subperiod, TierConfig, TierThreshold, + Perbill, PeriodNumber, Permill, Safeguard, StakerInfo, StaticTierParams, Subperiod, TierConfig, + TierThreshold, }; use frame_support::{ @@ -2407,7 +2408,7 @@ fn tier_config_recalculation_works() { let new_tier_config = TierConfig::<Test>::get(); assert!( - new_tier_config.number_of_slots > init_tier_config.number_of_slots, + new_tier_config.total_number_of_slots() > init_tier_config.total_number_of_slots(), "Price has increased, therefore number of slots must increase." ); assert_eq!( @@ -2415,9 +2416,22 @@ fn tier_config_recalculation_works() { new_tier_config.slots_per_tier.len(), "Sanity check." ); - for idx in 0..init_tier_config.slots_per_tier.len() { - assert!(init_tier_config.slots_per_tier[idx] < new_tier_config.slots_per_tier[idx]); - } + assert!( + new_tier_config + .slots_per_tier + .iter() + .zip(init_tier_config.slots_per_tier.iter()) + .all(|(new, init)| new > init), + "Number of slots per tier should increase with higher price" + ); + assert!( + new_tier_config + .tier_thresholds + .iter() + .zip(init_tier_config.tier_thresholds.iter()) + .all(|(new, init)| new <= init), + "Tier threshold values should decrease with higher price" + ); // 3. Decrease the native price, and expect slots in tiers to be decreased. NATIVE_PRICE.with(|v| *v.borrow_mut() = init_price * FixedU128::from_rational(1, 2)); @@ -2427,7 +2441,7 @@ fn tier_config_recalculation_works() { let new_tier_config = TierConfig::<Test>::get(); assert!( - new_tier_config.number_of_slots < init_tier_config.number_of_slots, + new_tier_config.total_number_of_slots() < init_tier_config.total_number_of_slots(), "Price has decreased, therefore number of slots must decrease." ); assert_eq!( @@ -2435,9 +2449,22 @@ fn tier_config_recalculation_works() { new_tier_config.slots_per_tier.len(), "Sanity check." ); - for idx in 0..init_tier_config.slots_per_tier.len() { - assert!(init_tier_config.slots_per_tier[idx] > new_tier_config.slots_per_tier[idx]); - } + assert!( + new_tier_config + .slots_per_tier + .iter() + .zip(init_tier_config.slots_per_tier.iter()) + .all(|(new, init)| new < init), + "Number of slots per tier should decrease with lower price" + ); + assert!( + new_tier_config + .tier_thresholds + .iter() + .zip(init_tier_config.tier_thresholds.iter()) + .all(|(new, init)| new >= init), + "Tier threshold values should increase with lower price" + ); }) } @@ -2446,7 +2473,6 @@ fn get_dapp_tier_assignment_and_rewards_basic_example_works() { ExtBuilder::build().execute_with(|| { // Tier config is specially adapted for this test. TierConfig::<Test>::mutate(|config| { - config.number_of_slots = 40; config.slots_per_tier = BoundedVec::try_from(vec![2, 5, 13, 20]).unwrap(); }); @@ -2481,7 +2507,7 @@ fn get_dapp_tier_assignment_and_rewards_basic_example_works() { lock_and_stake( dapp_index, &smart_contracts[dapp_index], - tier_config.tier_thresholds[0].threshold() + x + 1, + tier_config.tier_thresholds[0] + x + 1, ); dapp_index += 1; } @@ -2489,7 +2515,7 @@ fn get_dapp_tier_assignment_and_rewards_basic_example_works() { lock_and_stake( dapp_index, &smart_contracts[dapp_index], - tier_config.tier_thresholds[0].threshold(), + tier_config.tier_thresholds[0], ); dapp_index += 1; @@ -2497,7 +2523,7 @@ fn get_dapp_tier_assignment_and_rewards_basic_example_works() { lock_and_stake( dapp_index, &smart_contracts[dapp_index], - tier_config.tier_thresholds[0].threshold() - 1, + tier_config.tier_thresholds[0] - 1, ); dapp_index += 1; @@ -2507,7 +2533,7 @@ fn get_dapp_tier_assignment_and_rewards_basic_example_works() { lock_and_stake( dapp_index, &smart_contracts[dapp_index], - tier_config.tier_thresholds[3].threshold() + x, + tier_config.tier_thresholds[3] + x, ); dapp_index += 1; } @@ -2516,7 +2542,7 @@ fn get_dapp_tier_assignment_and_rewards_basic_example_works() { lock_and_stake( dapp_index, &smart_contracts[dapp_index], - tier_config.tier_thresholds[3].threshold() - 1, + tier_config.tier_thresholds[3] - 1, ); // Finally, the actual test @@ -2590,8 +2616,6 @@ fn get_dapp_tier_assignment_and_rewards_zero_slots_per_tier_works() { // Ensure that first tier has 0 slots. TierConfig::<Test>::mutate(|config| { - let slots_in_first_tier = config.slots_per_tier[0]; - config.number_of_slots = config.number_of_slots - slots_in_first_tier; config.slots_per_tier[0] = 0; }); @@ -3044,21 +3068,6 @@ fn safeguard_configurable_by_genesis_config() { Permill::from_percent(30), Permill::from_percent(40), ], - tier_thresholds: vec![ - TierThreshold::DynamicTvlAmount { - amount: 30000, - minimum_amount: 20000, - }, - TierThreshold::DynamicTvlAmount { - amount: 7500, - minimum_amount: 5000, - }, - TierThreshold::DynamicTvlAmount { - amount: 20000, - minimum_amount: 15000, - }, - TierThreshold::FixedTvlAmount { amount: 5000 }, - ], slots_per_tier: vec![10, 20, 30, 40], ..Default::default() }; @@ -3092,6 +3101,7 @@ fn safeguard_configurable_by_genesis_config() { fn base_number_of_slots_is_respected() { ExtBuilder::build().execute_with(|| { // 0. Get expected number of slots for the base price + let total_issuance = <Test as Config>::Currency::total_issuance(); let base_native_price = <Test as Config>::BaseNativeCurrencyPrice::get(); let base_number_of_slots = <Test as Config>::TierSlots::number_of_slots(base_native_price); @@ -3101,7 +3111,7 @@ fn base_number_of_slots_is_respected() { run_for_blocks(1); assert_eq!( - TierConfig::<Test>::get().number_of_slots, + TierConfig::<Test>::get().total_number_of_slots(), base_number_of_slots, "Base number of slots is expected for base native currency price." ); @@ -3115,21 +3125,26 @@ fn base_number_of_slots_is_respected() { run_for_blocks(1); assert!( - TierConfig::<Test>::get().number_of_slots > base_number_of_slots, + TierConfig::<Test>::get().total_number_of_slots() > base_number_of_slots, "Price has increased, therefore number of slots must increase." ); assert_eq!( - TierConfig::<Test>::get().number_of_slots, + TierConfig::<Test>::get().total_number_of_slots(), <Test as Config>::TierSlots::number_of_slots(higher_price), ); - for tier_threshold in TierConfig::<Test>::get().tier_thresholds.iter() { - if let TierThreshold::DynamicTvlAmount { - amount, - minimum_amount, - } = tier_threshold + for (amount, static_tier_threshold) in TierConfig::<Test>::get() + .tier_thresholds + .iter() + .zip(StaticTierParams::<Test>::get().tier_thresholds.iter()) + { + if let TierThreshold::DynamicPercentage { + minimum_required_percentage, + .. + } = static_tier_threshold { - assert_eq!(*amount, *minimum_amount, "Thresholds must be saturated."); + let minimum_amount = *minimum_required_percentage * total_issuance; + assert_eq!(*amount, minimum_amount, "Thresholds must be saturated."); } } @@ -3140,7 +3155,7 @@ fn base_number_of_slots_is_respected() { run_for_blocks(1); assert_eq!( - TierConfig::<Test>::get().number_of_slots, + TierConfig::<Test>::get().total_number_of_slots(), base_number_of_slots, "Base number of slots is expected for base native currency price." ); @@ -3158,11 +3173,11 @@ fn base_number_of_slots_is_respected() { run_for_blocks(1); assert!( - TierConfig::<Test>::get().number_of_slots < base_number_of_slots, + TierConfig::<Test>::get().total_number_of_slots() < base_number_of_slots, "Price has decreased, therefore number of slots must decrease." ); assert_eq!( - TierConfig::<Test>::get().number_of_slots, + TierConfig::<Test>::get().total_number_of_slots(), <Test as Config>::TierSlots::number_of_slots(lower_price), ); @@ -3173,7 +3188,7 @@ fn base_number_of_slots_is_respected() { run_for_blocks(1); assert_eq!( - TierConfig::<Test>::get().number_of_slots, + TierConfig::<Test>::get().total_number_of_slots(), base_number_of_slots, "Base number of slots is expected for base native currency price." ); @@ -3254,12 +3269,14 @@ fn ranking_will_calc_reward_correctly() { #[test] fn claim_dapp_reward_with_rank() { ExtBuilder::build().execute_with(|| { + let total_issuance = <Test as Config>::Currency::total_issuance(); + // Register smart contract, lock&stake some amount let smart_contract = MockSmartContract::wasm(1 as AccountId); assert_register(1, &smart_contract); let alice = 2; - let amount = 99; // very close to tier 0 so will enter tier 1 with rank 9 + let amount = Perbill::from_parts(11_000_000) * total_issuance; // very close to tier 0 so will enter tier 1 with rank 9 assert_lock(alice, amount); assert_stake(alice, &smart_contract, amount); diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 0ceaa3dc1..416985eb8 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -2810,26 +2810,6 @@ fn era_reward_span_fails_when_expected() { ); } -#[test] -fn tier_threshold_is_ok() { - let amount = 100; - - // Fixed TVL - let fixed_threshold = TierThreshold::FixedTvlAmount { amount }; - assert!(fixed_threshold.is_satisfied(amount)); - assert!(fixed_threshold.is_satisfied(amount + 1)); - assert!(!fixed_threshold.is_satisfied(amount - 1)); - - // Dynamic TVL - let dynamic_threshold = TierThreshold::DynamicTvlAmount { - amount, - minimum_amount: amount / 2, // not important - }; - assert!(dynamic_threshold.is_satisfied(amount)); - assert!(dynamic_threshold.is_satisfied(amount + 1)); - assert!(!dynamic_threshold.is_satisfied(amount - 1)); -} - #[test] fn tier_params_check_is_ok() { // Prepare valid params @@ -2848,15 +2828,15 @@ fn tier_params_check_is_ok() { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { - amount: 1000, - minimum_amount: 100, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(3), + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(2), }, - TierThreshold::DynamicTvlAmount { - amount: 100, - minimum_amount: 10, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(1), }, - TierThreshold::FixedTvlAmount { amount: 10 }, ]) .unwrap(), }; @@ -2900,8 +2880,10 @@ fn tier_params_check_is_ok() { // 4th scenario - incorrect vector length let mut new_params = params.clone(); - new_params.tier_thresholds = - BoundedVec::try_from(vec![TierThreshold::FixedTvlAmount { amount: 10 }]).unwrap(); + new_params.tier_thresholds = BoundedVec::try_from(vec![TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(1), + }]) + .unwrap(); assert!(!new_params.is_valid()); } @@ -2925,21 +2907,24 @@ fn tier_configuration_basic_tests() { ]) .unwrap(), tier_thresholds: BoundedVec::try_from(vec![ - TierThreshold::DynamicTvlAmount { - amount: 1000, - minimum_amount: 800, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_percent(12), + minimum_required_percentage: Perbill::from_percent(8), + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_percent(7), + minimum_required_percentage: Perbill::from_percent(5), }, - TierThreshold::DynamicTvlAmount { - amount: 500, - minimum_amount: 350, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_percent(4), + minimum_required_percentage: Perbill::from_percent(3), }, - TierThreshold::DynamicTvlAmount { - amount: 100, - minimum_amount: 70, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(3), }, - TierThreshold::FixedTvlAmount { amount: 50 }, ]) .unwrap(), + ..TierParameters::default() }; assert!(params.is_valid(), "Example params must be valid!"); @@ -2947,22 +2932,30 @@ fn tier_configuration_basic_tests() { parameter_types! { pub const BaseNativeCurrencyPrice: FixedU128 = FixedU128::from_rational(5, 100); } + let total_issuance: Balance = 9_000_000_000; + let tier_thresholds = params + .tier_thresholds + .iter() + .map(|t| t.threshold(total_issuance)) + .collect::<Vec<Balance>>() + .try_into() + .expect("Invalid number of tier thresholds provided."); + let init_config = TiersConfiguration::<TiersNum, StandardTierSlots, BaseNativeCurrencyPrice> { - 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(), + tier_thresholds, _phantom: Default::default(), }; assert!(init_config.is_valid(), "Init config must be valid!"); // Create a new config, based on a new price let high_price = FixedU128::from_rational(20, 100); // in production will be expressed in USD - let new_config = init_config.calculate_new(high_price, ¶ms); + let new_config = init_config.calculate_new(¶ms, high_price, total_issuance); assert!(new_config.is_valid()); let low_price = FixedU128::from_rational(1, 100); // in production will be expressed in USD - let new_config = init_config.calculate_new(low_price, ¶ms); + let new_config = init_config.calculate_new(¶ms, low_price, total_issuance); 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.) @@ -3107,3 +3100,30 @@ fn dapp_tier_rewards_with_rank() { )) ); } + +#[test] +fn tier_thresholds_conversion_test() { + get_u32_type!(TiersNum, 2); + let total_issuance: Balance = 1_000_000; + + let thresholds: BoundedVec<TierThreshold, TiersNum> = BoundedVec::try_from(vec![ + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(10), + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_percent(5), + minimum_required_percentage: Perbill::from_percent(2), + }, + ]) + .unwrap(); + + let tier_thresholds: BoundedVec<Balance, TiersNum> = thresholds + .iter() + .map(|t| t.threshold(total_issuance)) + .collect::<Vec<Balance>>() + .try_into() + .expect("Invalid number of tier thresholds provided."); + + assert_eq!(tier_thresholds[0], 100_000); // 10% of total issuance + assert_eq!(tier_thresholds[1], 50_000); // 5% of total issuance +} diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 7eaa76773..c8cd9d9c9 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -57,7 +57,7 @@ //! //! ## Tier Information //! -//! * `TierThreshold` - an enum describing tier entry thresholds. +//! * `TierThreshold` - an enum describing tier entry thresholds (as TVL amounts or as percentages of the total issuance). //! * `TierParameters` - contains static information about tiers, like init thresholds, reward & slot distribution. //! * `TiersConfiguration` - contains dynamic information about tiers, derived from `TierParameters` and onchain data. //! * `DAppTier` - a compact struct describing a dApp's tier. @@ -70,7 +70,7 @@ use serde::{Deserialize, Serialize}; use sp_arithmetic::fixed_point::FixedU128; use sp_runtime::{ traits::{CheckedAdd, UniqueSaturatedInto, Zero}, - FixedPointNumber, Permill, Saturating, + FixedPointNumber, Perbill, Permill, Saturating, }; pub use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, vec::Vec}; @@ -1467,32 +1467,26 @@ where Deserialize, )] 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. - /// The `amount` should never go below the `minimum_amount`. - DynamicTvlAmount { - amount: Balance, - minimum_amount: Balance, + /// Entry into the tier is mandated by a fixed percentage of the total issuance as staked funds. + /// This value is constant and does not change between periods. + FixedPercentage { required_percentage: Perbill }, + /// Entry into the tier is mandated by a percentage of the total issuance as staked funds. + /// The `percentage` is the amount required, which can change in-between periods, while `minimum_required_percentage` + /// is the minimum percentage that should not be reduced below. + DynamicPercentage { + percentage: Perbill, + minimum_required_percentage: Perbill, }, } impl TierThreshold { - /// Used to check if stake amount satisfies the threshold or not. - pub fn is_satisfied(&self, stake: Balance) -> bool { - match self { - Self::FixedTvlAmount { amount } => stake >= *amount, - Self::DynamicTvlAmount { amount, .. } => stake >= *amount, - } - } - - /// Return threshold for the tier. - pub fn threshold(&self) -> Balance { + /// Return threshold amount for the tier. + pub fn threshold(&self, total_issuance: Balance) -> Balance { match self { - Self::FixedTvlAmount { amount } => *amount, - Self::DynamicTvlAmount { amount, .. } => *amount, + Self::DynamicPercentage { percentage, .. } => *percentage * total_issuance, + Self::FixedPercentage { + required_percentage, + } => *required_percentage * total_issuance, } } } @@ -1585,9 +1579,6 @@ impl<NT: Get<u32>> Default for TierParameters<NT> { )] #[scale_info(skip_type_params(NT, T, P))] pub struct TiersConfiguration<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> { - /// 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<u16, NT>, @@ -1597,7 +1588,7 @@ pub struct TiersConfiguration<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> pub reward_portion: BoundedVec<Permill, NT>, /// Requirements for entry into each tier. /// First entry refers to the first tier, and so on. - pub tier_thresholds: BoundedVec<TierThreshold, NT>, + pub tier_thresholds: BoundedVec<Balance, NT>, /// Phantom data to keep track of the tier slots function. #[codec(skip)] pub(crate) _phantom: PhantomData<(T, P)>, @@ -1606,7 +1597,6 @@ pub struct TiersConfiguration<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> impl<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> Default for TiersConfiguration<NT, T, P> { fn default() -> Self { Self { - number_of_slots: 0, slots_per_tier: BoundedVec::default(), reward_portion: BoundedVec::default(), tier_thresholds: BoundedVec::default(), @@ -1628,13 +1618,22 @@ impl<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> TiersConfiguration<NT, T // All vector 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 + } + + /// Calculate the total number of slots. + pub fn total_number_of_slots(&self) -> u16 { + self.slots_per_tier.iter().copied().sum() } /// Calculate new `TiersConfiguration`, based on the old settings, current native currency price and tier configuration. - pub fn calculate_new(&self, native_price: FixedU128, params: &TierParameters<NT>) -> Self { + pub fn calculate_new( + &self, + params: &TierParameters<NT>, + native_price: FixedU128, + total_issuance: Balance, + ) -> Self { // It must always be at least 1 slot. + let base_number_of_slots = T::number_of_slots(P::get()).max(1); let new_number_of_slots = T::number_of_slots(native_price).max(1); // Calculate how much each tier gets slots. @@ -1649,11 +1648,25 @@ impl<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> TiersConfiguration<NT, T let new_slots_per_tier = BoundedVec::<u16, NT>::try_from(new_slots_per_tier).unwrap_or_default(); + // NOTE: even though we could ignore the situation when the new & base slot numbers are equal, it's necessary to re-calculate it since + // other params related to calculation might have changed. + let delta_threshold = if new_number_of_slots >= base_number_of_slots { + FixedU128::from_rational( + (new_number_of_slots - base_number_of_slots).into(), + new_number_of_slots.into(), + ) + } else { + FixedU128::from_rational( + (base_number_of_slots - new_number_of_slots).into(), + new_number_of_slots.into(), + ) + }; + // Update tier thresholds. // In case number of slots increase, we decrease thresholds required to enter the tier. // In case number of slots decrease, we increase the threshold required to enter the tier. // - // According to formula: %_threshold = (100% / (100% - delta_%_slots) - 1) * 100% + // According to formula: %delta_threshold = (100% / (100% - delta_%_slots) - 1) * 100% // // where delta_%_slots is simply: (base_num_slots - new_num_slots) / base_num_slots // @@ -1669,61 +1682,40 @@ impl<NT: Get<u32>, T: TierSlotsFunc, P: Get<FixedU128>> TiersConfiguration<NT, T // formulas are adjusted like: // // 1. Number of slots has increased, threshold is expected to decrease - // %_threshold = (new_num_slots - base_num_slots) / new_num_slots - // new_threshold = base_threshold * (1 - %_threshold) + // %delta_threshold = (new_num_slots - base_num_slots) / new_num_slots + // new_threshold = base_threshold * (1 - %delta_threshold) // // 2. Number of slots has decreased, threshold is expected to increase - // %_threshold = (base_num_slots - new_num_slots) / new_num_slots - // new_threshold = base_threshold * (1 + %_threshold) + // %delta_threshold = (base_num_slots - new_num_slots) / new_num_slots + // new_threshold = base_threshold * (1 + %delta_threshold) // - let base_number_of_slots = T::number_of_slots(P::get()).max(1); - - // NOTE: even though we could ignore the situation when the new & base slot numbers are equal, it's necessary to re-calculate it since - // other params related to calculation might have changed. - let new_tier_thresholds = if new_number_of_slots >= base_number_of_slots { - let delta_threshold_decrease = FixedU128::from_rational( - (new_number_of_slots - base_number_of_slots).into(), - new_number_of_slots.into(), - ); - - let mut new_tier_thresholds = params.tier_thresholds.clone(); - new_tier_thresholds - .iter_mut() - .for_each(|threshold| match threshold { - TierThreshold::DynamicTvlAmount { - amount, - minimum_amount, - } => { - *amount = amount - .saturating_sub(delta_threshold_decrease.saturating_mul_int(*amount)); - *amount = *amount.max(minimum_amount); - } - _ => (), - }); - - new_tier_thresholds - } else { - let delta_threshold_increase = FixedU128::from_rational( - (base_number_of_slots - new_number_of_slots).into(), - new_number_of_slots.into(), - ); - - let mut new_tier_thresholds = params.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 - }; + let new_tier_thresholds: BoundedVec<Balance, NT> = params + .tier_thresholds + .clone() + .iter() + .map(|threshold| match threshold { + TierThreshold::DynamicPercentage { + percentage, + minimum_required_percentage, + } => { + let amount = *percentage * total_issuance; + let adjusted_amount = if new_number_of_slots >= base_number_of_slots { + amount.saturating_sub(delta_threshold.saturating_mul_int(amount)) + } else { + amount.saturating_add(delta_threshold.saturating_mul_int(amount)) + }; + let minimum_amount = *minimum_required_percentage * total_issuance; + adjusted_amount.max(minimum_amount) + } + TierThreshold::FixedPercentage { + required_percentage, + } => *required_percentage * total_issuance, + }) + .collect::<Vec<_>>() + .try_into() + .unwrap_or_default(); 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, diff --git a/precompiles/dapp-staking-v3/src/test/mock.rs b/precompiles/dapp-staking-v3/src/test/mock.rs index 7266a69d3..a59b45612 100644 --- a/precompiles/dapp-staking-v3/src/test/mock.rs +++ b/precompiles/dapp-staking-v3/src/test/mock.rs @@ -36,7 +36,7 @@ use sp_core::{H160, H256}; use sp_io::TestExternalities; use sp_runtime::{ traits::{BlakeTwo256, ConstU32, IdentityLookup}, - BuildStorage, + BuildStorage, Perbill, }; extern crate alloc; @@ -316,19 +316,18 @@ impl ExternalityBuilder { Permill::from_percent(40), ], tier_thresholds: vec![ - TierThreshold::DynamicTvlAmount { - amount: 100, - minimum_amount: 80, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(4), }, - TierThreshold::DynamicTvlAmount { - amount: 50, - minimum_amount: 40, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(3), }, - TierThreshold::DynamicTvlAmount { - amount: 20, - minimum_amount: 20, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(2), + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_percent(1), }, - TierThreshold::FixedTvlAmount { amount: 10 }, ], slots_per_tier: vec![10, 20, 30, 40], safeguard: None, diff --git a/runtime/astar/src/lib.rs b/runtime/astar/src/lib.rs index 21922b03a..5d7136f82 100644 --- a/runtime/astar/src/lib.rs +++ b/runtime/astar/src/lib.rs @@ -1268,16 +1268,38 @@ pub type Executive = frame_executive::Executive< Migrations, >; +parameter_types! { + // percentages below are calulated based on total issuance at the time when dApp staking v3 was launched (8.4B) + pub const TierThresholds: [TierThreshold; 4] = [ + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(35_700_000), // 3.57% + minimum_required_percentage: Perbill::from_parts(23_800_000), // 2.38% + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(8_900_000), // 0.89% + minimum_required_percentage: Perbill::from_parts(6_000_000), // 0.6% + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(2_380_000), // 0.238% + minimum_required_percentage: Perbill::from_parts(1_790_000), // 0.179% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(200_000), // 0.02% + }, + ]; +} + /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. pub type Migrations = ( - cumulus_pallet_xcmp_queue::migration::v4::MigrationToV4<Runtime>, // permanent migration, do not remove pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>, // XCM V3 -> V4 pallet_xc_asset_config::migrations::versioned::V2ToV3<Runtime>, pallet_identity::migration::versioned::V0ToV1<Runtime, 250>, + // dapp-staking dyn tier threshold migrations + pallet_dapp_staking_v3::migration::versioned_migrations::V7ToV8<Runtime, TierThresholds>, ); type EventRecord = frame_system::EventRecord< diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 9dba42c6a..e28ea22f4 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -1609,6 +1609,27 @@ pub type Executive = frame_executive::Executive< Migrations, >; +parameter_types! { + // percentages below are calulated based on a total issuance at the time when dApp staking v3 was launched (147M) + pub const TierThresholds: [TierThreshold; 4] = [ + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(20_000), // 0.0020% + minimum_required_percentage: Perbill::from_parts(17_000), // 0.0017% + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(13_000), // 0.0013% + minimum_required_percentage: Perbill::from_parts(10_000), // 0.0010% + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(5_400), // 0.00054% + minimum_required_percentage: Perbill::from_parts(3_400), // 0.00034% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(1_400), // 0.00014% + }, + ]; +} + /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. @@ -1616,10 +1637,8 @@ pub type Migrations = ( cumulus_pallet_xcmp_queue::migration::v4::MigrationToV4<Runtime>, // permanent migration, do not remove pallet_xcm::migration::MigrateToLatestXcmVersion<Runtime>, - // XCM V3 -> V4 - pallet_xc_asset_config::migrations::versioned::V2ToV3<Runtime>, - pallet_identity::migration::versioned::V0ToV1<Runtime, 250>, - pallet_unified_accounts::migration::ClearCorruptedUnifiedMappings<Runtime>, + // dapp-staking dyn tier threshold migrations + pallet_dapp_staking_v3::migration::versioned_migrations::V7ToV8<Runtime, TierThresholds>, ); type EventRecord = frame_system::EventRecord< diff --git a/runtime/shiden/src/lib.rs b/runtime/shiden/src/lib.rs index 6432aaf76..ef219c5a7 100644 --- a/runtime/shiden/src/lib.rs +++ b/runtime/shiden/src/lib.rs @@ -1269,6 +1269,27 @@ pub type Executive = frame_executive::Executive< Migrations, >; +parameter_types! { + // percentages below are calulated based on a total issuance at the time when dApp staking v3 was launched (84.3M) + pub const TierThresholds: [TierThreshold; 4] = [ + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(35_700_000), // 3.57% + minimum_required_percentage: Perbill::from_parts(23_800_000), // 2.38% + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(8_900_000), // 0.89% + minimum_required_percentage: Perbill::from_parts(6_000_000), // 0.6% + }, + TierThreshold::DynamicPercentage { + percentage: Perbill::from_parts(2_380_000), // 0.238% + minimum_required_percentage: Perbill::from_parts(1_790_000), // 0.179% + }, + TierThreshold::FixedPercentage { + required_percentage: Perbill::from_parts(600_000), // 0.06% + }, + ]; +} + /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. @@ -1279,6 +1300,8 @@ pub type Migrations = ( // XCM V3 -> V4 pallet_xc_asset_config::migrations::versioned::V2ToV3<Runtime>, pallet_identity::migration::versioned::V0ToV1<Runtime, 250>, + // dapp-staking dyn tier threshold migrations + pallet_dapp_staking_v3::migration::versioned_migrations::V7ToV8<Runtime, TierThresholds>, ); type EventRecord = frame_system::EventRecord<