From 3d2d146f9f75cd863ed5abd134f95decd2670363 Mon Sep 17 00:00:00 2001 From: Dino Pacandi Date: Fri, 20 Oct 2023 17:13:22 +0200 Subject: [PATCH] Tier reward calculation WIP --- pallets/dapp-staking-v3/src/lib.rs | 93 +++++++++++++++++-- pallets/dapp-staking-v3/src/test/mock.rs | 39 +++++++- .../dapp-staking-v3/src/test/testing_utils.rs | 4 +- .../dapp-staking-v3/src/test/tests_types.rs | 22 ++++- pallets/dapp-staking-v3/src/types.rs | 60 ++++++------ 5 files changed, 172 insertions(+), 46 deletions(-) diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 348a101071..5973042aa7 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -45,6 +45,7 @@ use frame_support::{ weights::Weight, }; use frame_system::pallet_prelude::*; +use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ traits::{BadOrigin, Saturating, Zero}, Perbill, @@ -135,6 +136,10 @@ pub mod pallet { /// Minimum amount staker can stake on a contract. #[pallet::constant] type MinimumStakeAmount: Get; + + /// Number of different tiers. + #[pallet::constant] + type NumberOfTiers: Get; } #[pallet::event] @@ -285,6 +290,7 @@ pub mod pallet { #[pallet::storage] pub type NextDAppId = StorageValue<_, DAppId, ValueQuery>; + // TODO: where to track TierLabels? E.g. a label to bootstrap a dApp into a specific tier. /// Map of all dApps integrated into dApp staking protocol. #[pallet::storage] pub type IntegratedDApps = CountedStorageMap< @@ -339,6 +345,21 @@ pub mod pallet { pub type PeriodEnd = StorageMap<_, Twox64Concat, PeriodNumber, PeriodEndInfo, OptionQuery>; + /// Static tier parameters used to calculate tier configuration. + #[pallet::storage] + pub type StaticTierParams = + StorageValue<_, TierParameters, ValueQuery>; + + /// Tier configuration to be used during the newly started period + #[pallet::storage] + pub type NextTierConfig = + StorageValue<_, TierConfiguration, ValueQuery>; + + /// Tier configuration user for current & preceding eras. + #[pallet::storage] + pub type TierConfig = + StorageValue<_, TierConfiguration, ValueQuery>; + #[pallet::hooks] impl Hooks> for Pallet { fn on_initialize(now: BlockNumberFor) -> Weight { @@ -363,8 +384,11 @@ pub mod pallet { let (maybe_period_event, era_reward) = match protocol_state.period_type() { PeriodType::Voting => { // For the sake of consistency, we put zero reward into storage - let era_reward = - EraReward::new(Balance::zero(), era_info.total_staked_amount()); + let era_reward = EraReward { + staker_reward_pool: Balance::zero(), + staked: era_info.total_staked_amount(), + dapp_reward_pool: Balance::zero(), + }; let ending_era = next_era.saturating_add(T::StandardErasPerBuildAndEarnPeriod::get()); @@ -374,6 +398,10 @@ pub mod pallet { era_info.migrate_to_next_era(Some(protocol_state.period_type())); + // Update tier configuration to be used when calculating rewards for the upcoming eras + let next_tier_config = NextTierConfig::::take(); + TierConfig::::put(next_tier_config); + ( Some(Event::::NewPeriod { period_type: protocol_state.period_type(), @@ -383,11 +411,15 @@ pub mod pallet { ) } PeriodType::BuildAndEarn => { - // TODO: trigger dAPp tier reward calculation here. This will be implemented later. + // TODO: trigger dApp tier reward calculation here. This will be implemented later. let staker_reward_pool = Balance::from(1_000_000_000_000u128); // TODO: calculate this properly, inject it from outside (Tokenomics 2.0 pallet?) - let era_reward = - EraReward::new(staker_reward_pool, era_info.total_staked_amount()); + let dapp_reward_pool = Balance::from(1_000_000_000u128); // TODO: same as above + let era_reward = EraReward { + staker_reward_pool, + staked: era_info.total_staked_amount(), + dapp_reward_pool, + }; // Switch to `Voting` period if conditions are met. if protocol_state.period_info.is_next_period(next_era) { @@ -412,7 +444,12 @@ pub mod pallet { era_info.migrate_to_next_era(Some(protocol_state.period_type())); - // TODO: trigger tier configuration calculation based on internal & external params. + // Re-calculate tier configuration for the upcoming new period + let tier_params = StaticTierParams::::get(); + let average_price = FixedU64::from_rational(1, 10); // TODO: implement price fetching later + let new_tier_config = + TierConfig::::get().calculate_new(average_price, &tier_params); + NextTierConfig::::put(new_tier_config); ( Some(Event::::NewPeriod { @@ -1083,11 +1120,11 @@ pub mod pallet { .ok_or(Error::::InternalClaimStakerError)?; // Optimization, and zero-division protection - if amount.is_zero() || era_reward.staked().is_zero() { + if amount.is_zero() || era_reward.staked.is_zero() { continue; } - let staker_reward = Perbill::from_rational(amount, era_reward.staked()) - * era_reward.staker_reward_pool(); + let staker_reward = Perbill::from_rational(amount, era_reward.staked) + * era_reward.staker_reward_pool; rewards.push((era, staker_reward)); reward_sum.saturating_accrue(staker_reward); @@ -1234,7 +1271,7 @@ pub mod pallet { } /// `true` if smart contract is active, `false` if it has been unregistered. - fn is_active(smart_contract: &T::SmartContract) -> bool { + pub fn is_active(smart_contract: &T::SmartContract) -> bool { IntegratedDApps::::get(smart_contract) .map_or(false, |dapp_info| dapp_info.state == DAppState::Registered) } @@ -1243,5 +1280,41 @@ pub mod pallet { pub fn era_reward_span_index(era: EraNumber) -> EraNumber { era.saturating_sub(era % T::EraRewardSpanLength::get()) } + + // TODO - by breaking this into multiple steps, if they are too heavy for a single block, we can distribute them between multiple blocks. + pub fn dapp_tier_assignment(era: EraNumber, period: PeriodNumber) { + let tier_config = TierConfig::::get(); + // TODO: this really looks ugly, and too complicated. Botom line is, this value has to exist. If it doesn't we have to assume it's `Default`. + // Rewards will just end up being all zeroes. + let reward_info = EraRewards::::get(Self::era_reward_span_index(era)) + .map(|span| span.get(era).map(|x| *x).unwrap_or_default()) + .unwrap_or_default(); + + let mut dapp_stakes = Vec::with_capacity(IntegratedDApps::::count() as usize); + + // 1. + // This is bounded by max amount of dApps we allow to be registered + for (smart_contract, dapp_info) in IntegratedDApps::::iter() { + // Skip unregistered dApps + if dapp_info.state != DAppState::Registered { + continue; + } + + // Skip dApps which don't have ANY amount staked (TODO: potential improvement is to prune all dApps below minimum threshold) + let stake_amount = match ContractStake::::get(&smart_contract).get(era, period) { + Some(stake_amount) if !stake_amount.total().is_zero() => stake_amount, + _ => continue, + }; + + // TODO: maybe also push the 'Label' here? + dapp_stakes.push((dapp_info.id, stake_amount.total())); + } + + // 2. + // Sort by amount staked, in reverse - top dApp will end in the first place, 0th index. + dapp_stakes.sort_unstable_by(|(_, amount_1), (_, amount_2)| amount_2.cmp(amount_1)); + + // TODO: continue here + } } } diff --git a/pallets/dapp-staking-v3/src/test/mock.rs b/pallets/dapp-staking-v3/src/test/mock.rs index 35196ce042..79c6b5f69f 100644 --- a/pallets/dapp-staking-v3/src/test/mock.rs +++ b/pallets/dapp-staking-v3/src/test/mock.rs @@ -28,6 +28,7 @@ use sp_core::H256; use sp_runtime::{ testing::Header, traits::{BlakeTwo256, IdentityLookup}, + Permill, }; use sp_io::TestExternalities; @@ -119,6 +120,7 @@ impl pallet_dapp_staking::Config for Test { type MinimumLockedAmount = ConstU128; type UnlockingPeriod = ConstU64<20>; type MinimumStakeAmount = ConstU128<3>; + type NumberOfTiers = ConstU32<4>; } // TODO: why not just change this to e.g. u32 for test? @@ -174,8 +176,43 @@ impl ExtBuilder { maintenance: false, }); + // TODO: improve this laterm should be handled via genesis? + let tier_params = TierParameters::<::NumberOfTiers> { + 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(), + }; + let init_tier_config = TierConfiguration::<::NumberOfTiers> { + number_of_slots: 100, + 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(), + }; + + pallet_dapp_staking::StaticTierParams::::put(tier_params); + pallet_dapp_staking::TierConfig::::put(init_tier_config); + // DappStaking::on_initialize(System::block_number()); - }); + } + ); ext } diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 644a71f6e9..4483b483f5 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -781,8 +781,8 @@ pub(crate) fn assert_claim_staker_rewards(account: AccountId) { .get(era) .expect("Entry must exist, otherwise 'claim' is invalid."); - let reward = Perbill::from_rational(amount, era_reward_info.staked()) - * era_reward_info.staker_reward_pool(); + let reward = Perbill::from_rational(amount, era_reward_info.staked) + * era_reward_info.staker_reward_pool; if reward.is_zero() { continue; } diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 0eb4544dc1..48fb3df715 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -1469,7 +1469,11 @@ fn era_reward_span_push_and_get_works() { // Insert some values and verify state change let era_1 = 5; - let era_reward_1 = EraReward::new(23, 41); + let era_reward_1 = EraReward { + staker_reward_pool: 23, + staked: 41, + dapp_reward_pool: 17, + }; assert!(era_reward_span.push(era_1, era_reward_1).is_ok()); assert_eq!(era_reward_span.len(), 1); assert_eq!(era_reward_span.first_era(), era_1); @@ -1477,7 +1481,11 @@ fn era_reward_span_push_and_get_works() { // Insert another value and verify state change let era_2 = era_1 + 1; - let era_reward_2 = EraReward::new(37, 53); + let era_reward_2 = EraReward { + staker_reward_pool: 37, + staked: 53, + dapp_reward_pool: 19, + }; assert!(era_reward_span.push(era_2, era_reward_2).is_ok()); assert_eq!(era_reward_span.len(), 2); assert_eq!(era_reward_span.first_era(), era_1); @@ -1496,7 +1504,11 @@ fn era_reward_span_fails_when_expected() { // Push first values to get started let era_1 = 5; - let era_reward = EraReward::new(23, 41); + let era_reward = EraReward { + staker_reward_pool: 23, + staked: 41, + dapp_reward_pool: 17, + }; assert!(era_reward_span.push(era_1, era_reward).is_ok()); // Attempting to push incorrect era results in an error @@ -1521,7 +1533,7 @@ fn era_reward_span_fails_when_expected() { fn tier_slot_configuration_basic_tests() { // TODO: this should be expanded & improved later get_u32_type!(TiersNum, 4); - let params = TierSlotParameters:: { + let params = TierParameters:: { reward_portion: BoundedVec::try_from(vec![ Permill::from_percent(40), Permill::from_percent(30), @@ -1547,7 +1559,7 @@ fn tier_slot_configuration_basic_tests() { assert!(params.is_valid(), "Example params must be valid!"); // Create a configuration with some values - let init_config = TierSlotConfiguration:: { + let init_config = TierConfiguration:: { number_of_slots: 100, slots_per_tier: BoundedVec::try_from(vec![10, 20, 30, 40]).unwrap(), reward_portion: params.reward_portion.clone(), diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index 20a98e41e8..912019ce2a 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -288,7 +288,7 @@ where fn default() -> Self { Self { locked: Balance::zero(), - unlocking: BoundedVec::, UnlockingLen>::default(), + unlocking: BoundedVec::default(), staked: StakeAmount::default(), staked_future: None, staker_rewards_claimed: false, @@ -1239,30 +1239,13 @@ impl ContractStakeAmountSeries { pub struct EraReward { /// Total reward pool for staker rewards #[codec(compact)] - staker_reward_pool: Balance, + pub staker_reward_pool: Balance, /// Total amount which was staked at the end of an era #[codec(compact)] - staked: Balance, -} - -impl EraReward { - /// Create new instance of `EraReward` with specified `staker_reward_pool` and `staked` amounts. - pub fn new(staker_reward_pool: Balance, staked: Balance) -> Self { - Self { - staker_reward_pool, - staked, - } - } - - /// Total reward pool for staker rewards. - pub fn staker_reward_pool(&self) -> Balance { - self.staker_reward_pool - } - - /// Total amount which was staked at the end of an era. - pub fn staked(&self) -> Balance { - self.staked - } + pub staked: Balance, + /// Total reward pool for dApp rewards + #[codec(compact)] + pub dapp_reward_pool: Balance, } #[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] @@ -1375,7 +1358,7 @@ pub enum TierThreshold { /// 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> { +pub struct TierParameters> { /// 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. @@ -1389,7 +1372,7 @@ pub struct TierSlotParameters> { pub tier_thresholds: BoundedVec, } -impl> TierSlotParameters { +impl> TierParameters { /// 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 { @@ -1400,10 +1383,20 @@ impl> TierSlotParameters { } } +impl> Default for TierParameters { + fn default() -> Self { + Self { + reward_portion: BoundedVec::default(), + slot_distribution: BoundedVec::default(), + tier_thresholds: BoundedVec::default(), + } + } +} + /// Configuration of dApp tiers. #[derive(Encode, Decode, MaxEncodedLen, Clone, Debug, PartialEq, Eq, TypeInfo)] #[scale_info(skip_type_params(NT))] -pub struct TierSlotConfiguration> { +pub struct TierConfiguration> { /// Total number of slots. #[codec(compact)] pub number_of_slots: u16, @@ -1419,7 +1412,18 @@ pub struct TierSlotConfiguration> { pub tier_thresholds: BoundedVec, } -impl> TierSlotConfiguration { +impl> Default for TierConfiguration { + fn default() -> Self { + Self { + number_of_slots: 0, + slots_per_tier: BoundedVec::default(), + reward_portion: BoundedVec::default(), + tier_thresholds: BoundedVec::default(), + } + } +} + +impl> TierConfiguration { /// Check if parameters are valid. pub fn is_valid(&self) -> bool { let number_of_tiers: usize = NT::get() as usize; @@ -1432,7 +1436,7 @@ impl> TierSlotConfiguration { } /// TODO - pub fn calculate_new(&self, native_price: FixedU64, params: &TierSlotParameters) -> Self { + pub fn calculate_new(&self, native_price: FixedU64, params: &TierParameters) -> Self { let new_number_of_slots = Self::calculate_number_of_slots(native_price); // TODO: ugly, unsafe, refactor later