diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 9af355b25..a9f9d0c08 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -66,6 +66,8 @@ mod benchmarking; mod types; pub use types::*; +pub mod migrations; + pub mod weights; pub use weights::WeightInfo; @@ -343,8 +345,6 @@ pub mod pallet { /// No dApp tier info exists for the specified era. This can be because era has expired /// or because during the specified era there were no eligible rewards or protocol wasn't active. NoDAppTierInfo, - /// dApp reward has already been claimed for this era. - DAppRewardAlreadyClaimed, /// An unexpected error occured while trying to claim dApp reward. InternalClaimDAppError, /// Contract is still active, not unregistered. @@ -1322,7 +1322,6 @@ pub mod pallet { .try_claim(dapp_info.id) .map_err(|error| match error { DAppTierError::NoDAppInTiers => Error::::NoClaimableRewards, - DAppTierError::RewardAlreadyClaimed => Error::::DAppRewardAlreadyClaimed, _ => Error::::InternalClaimDAppError, })?; @@ -1673,7 +1672,7 @@ pub mod pallet { // Iterate over configured tier and potential dApps. // Each dApp will be assigned to the best possible tier if it satisfies the required condition, // and tier capacity hasn't been filled yet. - let mut dapp_tiers = Vec::with_capacity(dapp_stakes.len()); + let mut dapp_tiers = BTreeMap::new(); let tier_config = TierConfig::::get(); let mut global_idx = 0; @@ -1693,10 +1692,7 @@ pub mod pallet { for (dapp_id, stake_amount) in dapp_stakes[global_idx..max_idx].iter() { if tier_threshold.is_satisfied(*stake_amount) { global_idx.saturating_inc(); - dapp_tiers.push(DAppTier { - dapp_id: *dapp_id, - tier_id: Some(tier_id), - }); + dapp_tiers.insert(*dapp_id, tier_id); } else { break; } @@ -1988,106 +1984,3 @@ pub mod pallet { } } } - -/// `OnRuntimeUpgrade` logic used to set & configure init dApp staking v3 storage items. -pub struct DAppStakingV3InitConfig(PhantomData<(T, G)>); -impl< - T: Config, - G: Get<( - EraNumber, - TierParameters, - TiersConfiguration, - )>, - > OnRuntimeUpgrade for DAppStakingV3InitConfig -{ - fn on_runtime_upgrade() -> Weight { - if Pallet::::on_chain_storage_version() >= STORAGE_VERSION { - return T::DbWeight::get().reads(1); - } - - // 0. Unwrap arguments - let (init_era, tier_params, init_tier_config) = G::get(); - - // 1. Prepare init active protocol state - let now = frame_system::Pallet::::block_number(); - let voting_period_length = Pallet::::blocks_per_voting_period(); - - let period_number = 1; - let protocol_state = ProtocolState { - era: init_era, - next_era_start: now.saturating_add(voting_period_length), - period_info: PeriodInfo { - number: period_number, - subperiod: Subperiod::Voting, - next_subperiod_start_era: init_era.saturating_add(1), - }, - maintenance: true, - }; - - // 2. Prepare init current era info - need to set correct eras - let init_era_info = EraInfo { - total_locked: 0, - unlocking: 0, - current_stake_amount: StakeAmount { - voting: 0, - build_and_earn: 0, - era: init_era, - period: period_number, - }, - next_stake_amount: StakeAmount { - voting: 0, - build_and_earn: 0, - era: init_era.saturating_add(1), - period: period_number, - }, - }; - - // 3. Write necessary items into storage - ActiveProtocolState::::put(protocol_state); - StaticTierParams::::put(tier_params); - TierConfig::::put(init_tier_config); - STORAGE_VERSION.put::>(); - CurrentEraInfo::::put(init_era_info); - - // 4. Emit events to make indexers happy - Pallet::::deposit_event(Event::::NewEra { era: init_era }); - Pallet::::deposit_event(Event::::NewSubperiod { - subperiod: Subperiod::Voting, - number: 1, - }); - - log::info!("dApp Staking v3 storage initialized."); - - T::DbWeight::get().reads_writes(2, 5) - } - - #[cfg(feature = "try-runtime")] - fn post_upgrade(_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { - assert_eq!(Pallet::::on_chain_storage_version(), STORAGE_VERSION); - let protocol_state = ActiveProtocolState::::get(); - assert!(protocol_state.maintenance); - - let number_of_tiers = T::NumberOfTiers::get(); - - let tier_params = StaticTierParams::::get(); - assert_eq!(tier_params.reward_portion.len(), number_of_tiers as usize); - assert!(tier_params.is_valid()); - - let tier_config = TierConfig::::get(); - assert_eq!(tier_config.reward_portion.len(), number_of_tiers as usize); - assert_eq!(tier_config.slots_per_tier.len(), number_of_tiers as usize); - assert_eq!(tier_config.tier_thresholds.len(), number_of_tiers as usize); - - let current_era_info = CurrentEraInfo::::get(); - assert_eq!( - current_era_info.current_stake_amount.era, - protocol_state.era - ); - assert_eq!( - current_era_info.next_stake_amount.era, - protocol_state.era + 1 - ); - - Ok(()) - } -} diff --git a/pallets/dapp-staking-v3/src/migrations.rs b/pallets/dapp-staking-v3/src/migrations.rs new file mode 100644 index 000000000..5cb41d27e --- /dev/null +++ b/pallets/dapp-staking-v3/src/migrations.rs @@ -0,0 +1,202 @@ +// This file is part of Astar. + +// Copyright (C) 2019-2023 Stake Technologies Pte.Ltd. +// SPDX-License-Identifier: GPL-3.0-or-later + +// Astar is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Astar is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Astar. If not, see . + +use super::*; + +/// `OnRuntimeUpgrade` logic used to set & configure init dApp staking v3 storage items. +pub struct DAppStakingV3InitConfig(PhantomData<(T, G)>); +impl< + T: Config, + G: Get<( + EraNumber, + TierParameters, + TiersConfiguration, + )>, + > OnRuntimeUpgrade for DAppStakingV3InitConfig +{ + fn on_runtime_upgrade() -> Weight { + if Pallet::::on_chain_storage_version() >= STORAGE_VERSION { + return T::DbWeight::get().reads(1); + } + + // 0. Unwrap arguments + let (init_era, tier_params, init_tier_config) = G::get(); + + // 1. Prepare init active protocol state + let now = frame_system::Pallet::::block_number(); + let voting_period_length = Pallet::::blocks_per_voting_period(); + + let period_number = 1; + let protocol_state = ProtocolState { + era: init_era, + next_era_start: now.saturating_add(voting_period_length), + period_info: PeriodInfo { + number: period_number, + subperiod: Subperiod::Voting, + next_subperiod_start_era: init_era.saturating_add(1), + }, + maintenance: true, + }; + + // 2. Prepare init current era info - need to set correct eras + let init_era_info = EraInfo { + total_locked: 0, + unlocking: 0, + current_stake_amount: StakeAmount { + voting: 0, + build_and_earn: 0, + era: init_era, + period: period_number, + }, + next_stake_amount: StakeAmount { + voting: 0, + build_and_earn: 0, + era: init_era.saturating_add(1), + period: period_number, + }, + }; + + // 3. Write necessary items into storage + ActiveProtocolState::::put(protocol_state); + StaticTierParams::::put(tier_params); + TierConfig::::put(init_tier_config); + STORAGE_VERSION.put::>(); + CurrentEraInfo::::put(init_era_info); + + // 4. Emit events to make indexers happy + Pallet::::deposit_event(Event::::NewEra { era: init_era }); + Pallet::::deposit_event(Event::::NewSubperiod { + subperiod: Subperiod::Voting, + number: 1, + }); + + log::info!("dApp Staking v3 storage initialized."); + + T::DbWeight::get().reads_writes(2, 5) + } + + #[cfg(feature = "try-runtime")] + fn post_upgrade(_state: Vec) -> Result<(), sp_runtime::TryRuntimeError> { + assert_eq!(Pallet::::on_chain_storage_version(), STORAGE_VERSION); + let protocol_state = ActiveProtocolState::::get(); + assert!(protocol_state.maintenance); + + let number_of_tiers = T::NumberOfTiers::get(); + + let tier_params = StaticTierParams::::get(); + assert_eq!(tier_params.reward_portion.len(), number_of_tiers as usize); + assert!(tier_params.is_valid()); + + let tier_config = TierConfig::::get(); + assert_eq!(tier_config.reward_portion.len(), number_of_tiers as usize); + assert_eq!(tier_config.slots_per_tier.len(), number_of_tiers as usize); + assert_eq!(tier_config.tier_thresholds.len(), number_of_tiers as usize); + + let current_era_info = CurrentEraInfo::::get(); + assert_eq!( + current_era_info.current_stake_amount.era, + protocol_state.era + ); + assert_eq!( + current_era_info.next_stake_amount.era, + protocol_state.era + 1 + ); + + Ok(()) + } +} + +/// Legacy struct type +/// Should be deleted after the migration +#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] +struct OldDAppTier { + #[codec(compact)] + pub dapp_id: DAppId, + pub tier_id: Option, +} + +/// Information about all of the dApps that got into tiers, and tier rewards +#[derive( + Encode, + Decode, + MaxEncodedLen, + RuntimeDebugNoBound, + PartialEqNoBound, + EqNoBound, + CloneNoBound, + TypeInfo, +)] +#[scale_info(skip_type_params(MD, NT))] +struct OldDAppTierRewards, NT: Get> { + /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime) + pub dapps: BoundedVec, + /// Rewards for each tier. First entry refers to the first tier, and so on. + pub rewards: BoundedVec, + /// Period during which this struct was created. + #[codec(compact)] + pub period: PeriodNumber, +} + +impl, NT: Get> Default for OldDAppTierRewards { + fn default() -> Self { + Self { + dapps: BoundedVec::default(), + rewards: BoundedVec::default(), + period: 0, + } + } +} + +// Legacy convenience type for `DAppTierRewards` usage. +type OldDAppTierRewardsFor = + OldDAppTierRewards<::MaxNumberOfContracts, ::NumberOfTiers>; + +/// `OnRuntimeUpgrade` logic used to migrate DApp tiers storage item to BTreeMap. +pub struct DappStakingV3TierRewardAsTree(PhantomData); +impl OnRuntimeUpgrade for DappStakingV3TierRewardAsTree { + fn on_runtime_upgrade() -> Weight { + let mut counter = 0; + let mut translate = |pre: OldDAppTierRewardsFor| -> DAppTierRewardsFor { + let mut dapps_tree = BTreeMap::new(); + for dapp_tier in &pre.dapps { + if let Some(tier_id) = dapp_tier.tier_id { + dapps_tree.insert(dapp_tier.dapp_id, tier_id); + } + } + + let result = DAppTierRewardsFor::::new(dapps_tree, pre.rewards.to_vec(), pre.period); + if result.is_err() { + // Tests should ensure this never happens... + log::error!("Failed to migrate dApp tier rewards: {:?}", pre); + } + + // For weight calculation purposes + counter.saturating_inc(); + + // ...if it does happen, there's not much to do except create an empty map + result.unwrap_or( + DAppTierRewardsFor::::new(BTreeMap::new(), pre.rewards.to_vec(), pre.period) + .unwrap_or_default(), + ) + }; + + DAppTiers::::translate(|_key, value: OldDAppTierRewardsFor| Some(translate(value))); + + T::DbWeight::get().reads_writes(counter, counter) + } +} diff --git a/pallets/dapp-staking-v3/src/test/testing_utils.rs b/pallets/dapp-staking-v3/src/test/testing_utils.rs index 5e2e0af0d..ea1dfd18f 100644 --- a/pallets/dapp-staking-v3/src/test/testing_utils.rs +++ b/pallets/dapp-staking-v3/src/test/testing_utils.rs @@ -935,13 +935,13 @@ pub(crate) fn assert_claim_dapp_reward( let pre_total_issuance = ::Currency::total_issuance(); let pre_free_balance = ::Currency::free_balance(beneficiary); + let pre_reward_info = pre_snapshot + .dapp_tiers + .get(&era) + .expect("Entry must exist.") + .clone(); let (expected_reward, expected_tier_id) = { - let mut info = pre_snapshot - .dapp_tiers - .get(&era) - .expect("Entry must exist.") - .clone(); - + let mut info = pre_reward_info.clone(); info.try_claim(dapp_info.id).unwrap() }; @@ -976,16 +976,21 @@ pub(crate) fn assert_claim_dapp_reward( ); let post_snapshot = MemorySnapshot::new(); - let mut info = post_snapshot + let mut post_reward_info = post_snapshot .dapp_tiers .get(&era) .expect("Entry must exist.") .clone(); assert_eq!( - info.try_claim(dapp_info.id), - Err(DAppTierError::RewardAlreadyClaimed), + post_reward_info.try_claim(dapp_info.id), + Err(DAppTierError::NoDAppInTiers), "It must not be possible to claim the same reward twice!.", ); + assert_eq!( + pre_reward_info.dapps.len(), + post_reward_info.dapps.len() + 1, + "Entry must have been removed after successfull reward claim." + ); } /// Unstake some funds from the specified unregistered smart contract. diff --git a/pallets/dapp-staking-v3/src/test/tests.rs b/pallets/dapp-staking-v3/src/test/tests.rs index 12a06baeb..8db348736 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -1792,7 +1792,7 @@ fn claim_dapp_reward_twice_for_same_era_fails() { smart_contract, claim_era_1 ), - Error::::DAppRewardAlreadyClaimed, + Error::::NoClaimableRewards, ); // We can still claim for another valid era @@ -2299,34 +2299,25 @@ fn get_dapp_tier_assignment_basic_example_works() { assert_eq!(counter, number_of_smart_contracts); // 1st tier checks - let (entry_1, entry_2) = (tier_assignment.dapps[0], tier_assignment.dapps[1]); - assert_eq!(entry_1.dapp_id, 0); - assert_eq!(entry_1.tier_id, Some(0)); - - assert_eq!(entry_2.dapp_id, 1); - assert_eq!(entry_2.tier_id, Some(0)); + let (dapp_1_tier, dapp_2_tier) = (tier_assignment.dapps[&0], tier_assignment.dapps[&1]); + assert_eq!(dapp_1_tier, 0); + assert_eq!(dapp_2_tier, 0); // 2nd tier checks - let (entry_3, entry_4) = (tier_assignment.dapps[2], tier_assignment.dapps[3]); - assert_eq!(entry_3.dapp_id, 2); - assert_eq!(entry_3.tier_id, Some(1)); - - assert_eq!(entry_4.dapp_id, 3); - assert_eq!(entry_4.tier_id, Some(1)); + let (dapp_3_tier, dapp_4_tier) = (tier_assignment.dapps[&2], tier_assignment.dapps[&3]); + assert_eq!(dapp_3_tier, 1); + assert_eq!(dapp_4_tier, 1); // 4th tier checks - let (entry_5, entry_6) = (tier_assignment.dapps[4], tier_assignment.dapps[5]); - assert_eq!(entry_5.dapp_id, 4); - assert_eq!(entry_5.tier_id, Some(3)); - - assert_eq!(entry_6.dapp_id, 5); - assert_eq!(entry_6.tier_id, Some(3)); + let (dapp_5_tier, dapp_6_tier) = (tier_assignment.dapps[&4], tier_assignment.dapps[&5]); + assert_eq!(dapp_5_tier, 3); + assert_eq!(dapp_6_tier, 3); // Sanity check - last dapp should not exists in the tier assignment assert!(tier_assignment .dapps - .binary_search_by(|x| x.dapp_id.cmp(&(dapp_index.try_into().unwrap()))) - .is_err()); + .get(&dapp_index.try_into().unwrap()) + .is_none()); // Check that rewards are calculated correctly tier_config diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 8bf9183b6..61c1740b5 100644 --- a/pallets/dapp-staking-v3/src/test/tests_types.rs +++ b/pallets/dapp-staking-v3/src/test/tests_types.rs @@ -2685,28 +2685,7 @@ fn dapp_tier_rewards_basic_tests() { get_u32_type!(NumberOfTiers, 3); // Example dApps & rewards - let dapps = vec![ - DAppTier { - dapp_id: 1, - tier_id: Some(0), - }, - DAppTier { - dapp_id: 2, - tier_id: Some(0), - }, - DAppTier { - dapp_id: 3, - tier_id: Some(1), - }, - DAppTier { - dapp_id: 5, - tier_id: Some(1), - }, - DAppTier { - dapp_id: 6, - tier_id: Some(2), - }, - ]; + let dapps = BTreeMap::from([(1 as DAppId, 0 as TierId), (2, 0), (3, 1), (5, 1), (6, 2)]); let tier_rewards = vec![300, 20, 1]; let period = 2; @@ -2718,22 +2697,22 @@ fn dapp_tier_rewards_basic_tests() { .expect("Bounds are respected."); // 1st scenario - claim reward for a dApps - let tier_id = dapps[0].tier_id.unwrap(); + let tier_id = dapps[&1]; assert_eq!( - dapp_tier_rewards.try_claim(dapps[0].dapp_id), + dapp_tier_rewards.try_claim(1), Ok((tier_rewards[tier_id as usize], tier_id)) ); - let tier_id = dapps[3].tier_id.unwrap(); + let tier_id = dapps[&5]; assert_eq!( - dapp_tier_rewards.try_claim(dapps[3].dapp_id), + dapp_tier_rewards.try_claim(5), Ok((tier_rewards[tier_id as usize], tier_id)) ); // 2nd scenario - try to claim already claimed reward assert_eq!( - dapp_tier_rewards.try_claim(dapps[0].dapp_id), - Err(DAppTierError::RewardAlreadyClaimed), + dapp_tier_rewards.try_claim(1), + Err(DAppTierError::NoDAppInTiers), "Cannot claim the same reward twice." ); diff --git a/pallets/dapp-staking-v3/src/types.rs b/pallets/dapp-staking-v3/src/types.rs index cf70f0a6a..674d62f09 100644 --- a/pallets/dapp-staking-v3/src/types.rs +++ b/pallets/dapp-staking-v3/src/types.rs @@ -64,14 +64,14 @@ //! * `DAppTierRewards` - composite of `DAppTier` objects, describing the entire reward distribution for a particular era. //! -use frame_support::{pallet_prelude::*, BoundedVec}; +use frame_support::{pallet_prelude::*, BoundedBTreeMap, BoundedVec}; use parity_scale_codec::{Decode, Encode}; use sp_arithmetic::fixed_point::FixedU64; use sp_runtime::{ traits::{CheckedAdd, UniqueSaturatedInto, Zero}, FixedPointNumber, Permill, Saturating, }; -pub use sp_std::{fmt::Debug, vec::Vec}; +pub use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, vec::Vec}; use astar_primitives::{Balance, BlockNumber}; @@ -1644,18 +1644,6 @@ impl> TiersConfiguration { } } -/// Used to represent into which tier does a particular dApp fall into. -/// -/// In case tier Id is `None`, it means that either reward was already claimed, or dApp is not eligible for rewards. -#[derive(Encode, Decode, MaxEncodedLen, Copy, Clone, Debug, PartialEq, Eq, TypeInfo)] -pub struct DAppTier { - /// Unique dApp id in dApp staking protocol. - #[codec(compact)] - pub dapp_id: DAppId, - /// `Some(tier_id)` if dApp belongs to tier and has unclaimed rewards, `None` otherwise. - pub tier_id: Option, -} - /// Information about all of the dApps that got into tiers, and tier rewards #[derive( Encode, @@ -1670,7 +1658,7 @@ pub struct DAppTier { #[scale_info(skip_type_params(MD, NT))] pub struct DAppTierRewards, NT: Get> { /// DApps and their corresponding tiers (or `None` if they have been claimed in the meantime) - pub dapps: BoundedVec, + pub dapps: BoundedBTreeMap, /// Rewards for each tier. First entry refers to the first tier, and so on. pub rewards: BoundedVec, /// Period during which this struct was created. @@ -1681,7 +1669,7 @@ pub struct DAppTierRewards, NT: Get> { impl, NT: Get> Default for DAppTierRewards { fn default() -> Self { Self { - dapps: BoundedVec::default(), + dapps: BoundedBTreeMap::default(), rewards: BoundedVec::default(), period: 0, } @@ -1692,15 +1680,11 @@ impl, NT: Get> DAppTierRewards { /// Attempt to construct `DAppTierRewards` struct. /// If the provided arguments exceed the allowed capacity, return an error. pub fn new( - dapps: Vec, + dapps: BTreeMap, rewards: Vec, period: PeriodNumber, ) -> Result { - // Sort by dApp ID, in ascending order (unstable sort should be faster, and stability is "guaranteed" due to lack of duplicated Ids). - let mut dapps = dapps; - dapps.sort_unstable_by(|first, second| first.dapp_id.cmp(&second.dapp_id)); - - let dapps = BoundedVec::try_from(dapps).map_err(|_| ())?; + let dapps = BoundedBTreeMap::try_from(dapps).map_err(|_| ())?; let rewards = BoundedVec::try_from(rewards).map_err(|_| ())?; Ok(Self { dapps, @@ -1713,27 +1697,17 @@ impl, NT: Get> DAppTierRewards { /// In case dapp isn't applicable for rewards, or they have already been consumed, returns `None`. pub fn try_claim(&mut self, dapp_id: DAppId) -> Result<(Balance, TierId), DAppTierError> { // Check if dApp Id exists. - let dapp_idx = self + let tier_id = self .dapps - .binary_search_by(|entry| entry.dapp_id.cmp(&dapp_id)) - .map_err(|_| DAppTierError::NoDAppInTiers)?; - - match self.dapps.get_mut(dapp_idx) { - Some(dapp_tier) => { - if let Some(tier_id) = dapp_tier.tier_id.take() { - Ok(( - self.rewards - .get(tier_id as usize) - .map_or(Balance::zero(), |x| *x), - tier_id, - )) - } else { - Err(DAppTierError::RewardAlreadyClaimed) - } - } - // unreachable code, at this point it was proved that index exists - _ => Err(DAppTierError::InternalError), - } + .remove(&dapp_id) + .ok_or(DAppTierError::NoDAppInTiers)?; + + Ok(( + self.rewards + .get(tier_id as usize) + .map_or(Balance::zero(), |x| *x), + tier_id, + )) } } @@ -1741,8 +1715,6 @@ impl, NT: Get> DAppTierRewards { pub enum DAppTierError { /// Specified dApp Id doesn't exist in any tier. NoDAppInTiers, - /// Reward has already been claimed for this dApp. - RewardAlreadyClaimed, /// Internal, unexpected error occured. InternalError, } diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index b98dcae09..ad375e367 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -1380,7 +1380,7 @@ pub type Executive = frame_executive::Executive< /// All migrations that will run on the next runtime upgrade. /// /// Once done, migrations should be removed from the tuple. -pub type Migrations = (); +pub type Migrations = (pallet_dapp_staking_v3::migrations::DappStakingV3TierRewardAsTree,); type EventRecord = frame_system::EventRecord< ::RuntimeEvent,