diff --git a/Cargo.lock b/Cargo.lock index caa6bbb7b1..b8e727b1bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,7 @@ dependencies = [ "pallet-xc-asset-config", "parity-scale-codec", "scale-info", + "sp-arithmetic", "sp-core", "sp-io", "sp-runtime", @@ -2465,6 +2466,7 @@ dependencies = [ "astar-primitives", "pallet-dapp-staking-v3", "sp-api", + "sp-std", ] [[package]] @@ -6085,6 +6087,7 @@ dependencies = [ "pallet-preimage", "pallet-proxy", "pallet-scheduler", + "pallet-static-price-provider", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -8724,6 +8727,25 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-static-price-provider" +version = "0.1.0" +dependencies = [ + "astar-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-sudo" version = "4.0.0-dev" @@ -13234,6 +13256,7 @@ dependencies = [ "pallet-proxy", "pallet-scheduler", "pallet-session", + "pallet-static-price-provider", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", diff --git a/Cargo.toml b/Cargo.toml index 736c61ccfb..30f78873e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -284,6 +284,7 @@ pallet-inflation = { path = "./pallets/inflation", default-features = false } pallet-dynamic-evm-base-fee = { path = "./pallets/dynamic-evm-base-fee", default-features = false } pallet-unified-accounts = { path = "./pallets/unified-accounts", default-features = false } astar-xcm-benchmarks = { path = "./pallets/astar-xcm-benchmarks", default-features = false } +pallet-static-price-provider = { path = "./pallets/static-price-provider", default-features = false } dapp-staking-v3-runtime-api = { path = "./pallets/dapp-staking-v3/rpc/runtime-api", default-features = false } diff --git a/pallets/dapp-staking-migration/src/mock.rs b/pallets/dapp-staking-migration/src/mock.rs index c33aa09620..5da78a43e0 100644 --- a/pallets/dapp-staking-migration/src/mock.rs +++ b/pallets/dapp-staking-migration/src/mock.rs @@ -31,6 +31,7 @@ use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; use astar_primitives::{ dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, + oracle::PriceProvider, testing::Header, Balance, BlockNumber, }; @@ -108,7 +109,7 @@ impl pallet_balances::Config for Test { } pub struct DummyPriceProvider; -impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { +impl PriceProvider for DummyPriceProvider { fn average_price() -> FixedU64 { FixedU64::from_rational(1, 10) } diff --git a/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml b/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml index 559d5f299b..63ef539a0c 100644 --- a/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml +++ b/pallets/dapp-staking-v3/rpc/runtime-api/Cargo.toml @@ -12,6 +12,7 @@ targets = ["x86_64-unknown-linux-gnu"] [dependencies] sp-api = { workspace = true } +sp-std = { workspace = true } astar-primitives = { workspace = true } pallet-dapp-staking-v3 = { workspace = true } @@ -20,6 +21,7 @@ pallet-dapp-staking-v3 = { workspace = true } default = ["std"] std = [ "sp-api/std", + "sp-std/std", "pallet-dapp-staking-v3/std", "astar-primitives/std", ] diff --git a/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs b/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs index dde32bf695..d829e2b275 100644 --- a/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs +++ b/pallets/dapp-staking-v3/rpc/runtime-api/src/lib.rs @@ -19,7 +19,8 @@ #![cfg_attr(not(feature = "std"), no_std)] use astar_primitives::BlockNumber; -use pallet_dapp_staking_v3::EraNumber; +use pallet_dapp_staking_v3::{DAppId, EraNumber, PeriodNumber, TierId}; +pub use sp_std::collections::btree_map::BTreeMap; sp_api::decl_runtime_apis! { @@ -28,6 +29,9 @@ sp_api::decl_runtime_apis! { /// Used to provide information otherwise not available via RPC. pub trait DappStakingApi { + /// How many periods are there in one cycle. + fn periods_per_cycle() -> PeriodNumber; + /// For how many standard era lengths does the voting subperiod last. fn eras_per_voting_subperiod() -> EraNumber; @@ -36,5 +40,8 @@ sp_api::decl_runtime_apis! { /// How many blocks are there per standard era. fn blocks_per_era() -> BlockNumber; + + /// Get dApp tier assignment for the given dApp. + fn get_dapp_tier_assignment() -> BTreeMap; } } diff --git a/pallets/dapp-staking-v3/src/benchmarking/mod.rs b/pallets/dapp-staking-v3/src/benchmarking/mod.rs index 11663401d9..abb37ee071 100644 --- a/pallets/dapp-staking-v3/src/benchmarking/mod.rs +++ b/pallets/dapp-staking-v3/src/benchmarking/mod.rs @@ -946,8 +946,11 @@ mod benchmarks { #[block] { - let (dapp_tiers, _) = - Pallet::::get_dapp_tier_assignment(reward_era, reward_period, reward_pool); + let (dapp_tiers, _) = Pallet::::get_dapp_tier_assignment_and_rewards( + reward_era, + reward_period, + reward_pool, + ); assert_eq!(dapp_tiers.dapps.len(), x as usize); } } diff --git a/pallets/dapp-staking-v3/src/lib.rs b/pallets/dapp-staking-v3/src/lib.rs index 9af355b25c..81f50d8776 100644 --- a/pallets/dapp-staking-v3/src/lib.rs +++ b/pallets/dapp-staking-v3/src/lib.rs @@ -52,6 +52,7 @@ pub use sp_std::vec::Vec; use astar_primitives::{ dapp_staking::{CycleConfiguration, SmartContractHandle, StakingRewardHandler}, + oracle::PriceProvider, Balance, BlockNumber, }; @@ -66,6 +67,8 @@ mod benchmarking; mod types; pub use types::*; +pub mod migrations; + pub mod weights; pub use weights::WeightInfo; @@ -343,8 +346,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. @@ -561,6 +562,30 @@ pub mod pallet { #[pallet::call] impl Pallet { + /// Wrapper around _legacy-like_ `unbond_and_unstake`. + /// + /// Used to support legacy Ledger users so they can start the unlocking process for their funds. + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::unlock())] + pub fn unbond_and_unstake( + origin: OriginFor, + _contract_id: T::SmartContract, + #[pallet::compact] value: Balance, + ) -> DispatchResult { + // Once new period begins, all stakes are reset to zero, so all it remains to be done is the `unlock` + Self::unlock(origin, value) + } + + /// Wrapper around _legacy-like_ `withdraw_unbonded`. + /// + /// Used to support legacy Ledger users so they can reclaim unlocked chunks back into + /// their _transferable_ free balance. + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::claim_unlocked(T::MaxNumberOfStakedContracts::get()))] + pub fn withdraw_unbonded(origin: OriginFor) -> DispatchResultWithPostInfo { + Self::claim_unlocked(origin) + } + /// Used to enable or disable maintenance mode. /// Can only be called by manager origin. #[pallet::call_index(0)] @@ -706,7 +731,7 @@ pub mod pallet { /// This doesn't remove the dApp completely from the system just yet, but it can no longer be used for staking. /// /// Can be called by dApp staking manager origin. - #[pallet::call_index(4)] + #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::unregister())] pub fn unregister( origin: OriginFor, @@ -744,7 +769,7 @@ pub mod pallet { /// After adjustment, lock amount must be greater than zero and in total must be equal or greater than the minimum locked amount. /// /// Locked amount can immediately be used for staking. - #[pallet::call_index(5)] + #[pallet::call_index(7)] #[pallet::weight(T::WeightInfo::lock())] pub fn lock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; @@ -783,7 +808,7 @@ pub mod pallet { /// Only the amount that isn't actively used for staking can be unlocked. /// If the amount is greater than the available amount for unlocking, everything is unlocked. /// If the remaining locked amount would take the account below the minimum locked amount, everything is unlocked. - #[pallet::call_index(6)] + #[pallet::call_index(8)] #[pallet::weight(T::WeightInfo::unlock())] pub fn unlock(origin: OriginFor, #[pallet::compact] amount: Balance) -> DispatchResult { Self::ensure_pallet_enabled()?; @@ -836,7 +861,7 @@ pub mod pallet { } /// Claims all of fully unlocked chunks, removing the lock from them. - #[pallet::call_index(7)] + #[pallet::call_index(9)] #[pallet::weight(T::WeightInfo::claim_unlocked(T::MaxNumberOfStakedContracts::get()))] pub fn claim_unlocked(origin: OriginFor) -> DispatchResultWithPostInfo { Self::ensure_pallet_enabled()?; @@ -866,7 +891,7 @@ pub mod pallet { Ok(Some(T::WeightInfo::claim_unlocked(removed_entries)).into()) } - #[pallet::call_index(8)] + #[pallet::call_index(10)] #[pallet::weight(T::WeightInfo::relock_unlocking())] pub fn relock_unlocking(origin: OriginFor) -> DispatchResult { Self::ensure_pallet_enabled()?; @@ -903,7 +928,7 @@ pub mod pallet { /// and same for `Build&Earn` subperiod. /// /// Staked amount is only eligible for rewards from the next era onwards. - #[pallet::call_index(9)] + #[pallet::call_index(11)] #[pallet::weight(T::WeightInfo::stake())] pub fn stake( origin: OriginFor, @@ -1030,7 +1055,7 @@ pub mod pallet { /// In case amount is unstaked during `Voting` subperiod, the `voting` amount is reduced. /// In case amount is unstaked during `Build&Earn` subperiod, first the `build_and_earn` is reduced, /// and any spillover is subtracted from the `voting` amount. - #[pallet::call_index(10)] + #[pallet::call_index(12)] #[pallet::weight(T::WeightInfo::unstake())] pub fn unstake( origin: OriginFor, @@ -1132,7 +1157,7 @@ pub mod pallet { /// Claims some staker rewards, if user has any. /// In the case of a successfull call, at least one era will be claimed, with the possibility of multiple claims happening. - #[pallet::call_index(11)] + #[pallet::call_index(13)] #[pallet::weight({ let max_span_length = T::EraRewardSpanLength::get(); T::WeightInfo::claim_staker_rewards_ongoing_period(max_span_length) @@ -1226,7 +1251,7 @@ pub mod pallet { } /// Used to claim bonus reward for a smart contract, if eligible. - #[pallet::call_index(12)] + #[pallet::call_index(14)] #[pallet::weight(T::WeightInfo::claim_bonus_reward())] pub fn claim_bonus_reward( origin: OriginFor, @@ -1291,7 +1316,7 @@ pub mod pallet { } /// Used to claim dApp reward for the specified era. - #[pallet::call_index(13)] + #[pallet::call_index(15)] #[pallet::weight(T::WeightInfo::claim_dapp_reward())] pub fn claim_dapp_reward( origin: OriginFor, @@ -1322,7 +1347,6 @@ pub mod pallet { .try_claim(dapp_info.id) .map_err(|error| match error { DAppTierError::NoDAppInTiers => Error::::NoClaimableRewards, - DAppTierError::RewardAlreadyClaimed => Error::::DAppRewardAlreadyClaimed, _ => Error::::InternalClaimDAppError, })?; @@ -1347,7 +1371,7 @@ pub mod pallet { /// Used to unstake funds from a contract that was unregistered after an account staked on it. /// This is required if staker wants to re-stake these funds on another active contract during the ongoing period. - #[pallet::call_index(14)] + #[pallet::call_index(16)] #[pallet::weight(T::WeightInfo::unstake_from_unregistered())] pub fn unstake_from_unregistered( origin: OriginFor, @@ -1416,7 +1440,7 @@ pub mod pallet { /// Entry is considered to be expired if: /// 1. It's from a past period & the account wasn't a loyal staker, meaning there's no claimable bonus reward. /// 2. It's from a period older than the oldest claimable period, regardless whether the account was loyal or not. - #[pallet::call_index(15)] + #[pallet::call_index(17)] #[pallet::weight(T::WeightInfo::cleanup_expired_entries( T::MaxNumberOfStakedContracts::get() ))] @@ -1481,7 +1505,7 @@ pub mod pallet { /// Not intended to be used in production, except in case of unforseen circumstances. /// /// Can only be called by manager origin. - #[pallet::call_index(16)] + #[pallet::call_index(18)] #[pallet::weight(T::WeightInfo::force())] pub fn force(origin: OriginFor, forcing_type: ForcingType) -> DispatchResult { Self::ensure_pallet_enabled()?; @@ -1613,6 +1637,19 @@ pub mod pallet { T::CycleConfiguration::blocks_per_era().saturating_mul(T::UnlockingPeriod::get().into()) } + /// Returns the dApp tier assignment for the current era, based on the current stake amounts. + pub fn get_dapp_tier_assignment() -> BTreeMap { + let protocol_state = ActiveProtocolState::::get(); + + let (dapp_tiers, _count) = Self::get_dapp_tier_assignment_and_rewards( + protocol_state.era, + protocol_state.period_number(), + Balance::zero(), + ); + + dapp_tiers.dapps.into_inner() + } + /// Assign eligible dApps into appropriate tiers, and calculate reward for each tier. /// /// ### Algorithm @@ -1642,7 +1679,7 @@ pub mod pallet { /// /// The returned object contains information about each dApp that made it into a tier. /// Alongside tier assignment info, number of read DB contract stake entries is returned. - pub(crate) fn get_dapp_tier_assignment( + pub(crate) fn get_dapp_tier_assignment_and_rewards( era: EraNumber, period: PeriodNumber, dapp_reward_pool: Balance, @@ -1673,7 +1710,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 +1730,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; } @@ -1814,7 +1848,7 @@ pub mod pallet { // To help with benchmarking, it's possible to omit real tier calculation using the `Dummy` approach. // This must never be used in production code, obviously. let (dapp_tier_rewards, counter) = match tier_assignment { - TierAssignment::Real => Self::get_dapp_tier_assignment( + TierAssignment::Real => Self::get_dapp_tier_assignment_and_rewards( current_era, protocol_state.period_number(), dapp_reward_pool, @@ -1988,106 +2022,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 0000000000..5cb41d27e1 --- /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 5e2e0af0d5..ea1dfd18f9 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 12a06baebc..a6f8a9700f 100644 --- a/pallets/dapp-staking-v3/src/test/tests.rs +++ b/pallets/dapp-staking-v3/src/test/tests.rs @@ -156,6 +156,18 @@ fn maintenace_mode_call_filtering_works() { DappStaking::force(RuntimeOrigin::root(), ForcingType::Era), Error::::Disabled ); + assert_noop!( + DappStaking::unbond_and_unstake( + RuntimeOrigin::signed(1), + MockSmartContract::wasm(1 as AccountId), + 100 + ), + Error::::Disabled + ); + assert_noop!( + DappStaking::withdraw_unbonded(RuntimeOrigin::signed(1),), + Error::::Disabled + ); }) } @@ -503,6 +515,29 @@ fn lock_with_incorrect_amount_fails() { }) } +#[test] +fn unbond_and_unstake_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock some amount + let account = 2; + let lock_amount = 101; + assert_lock(account, lock_amount); + + // 'unbond_and_unstake' some amount, assert expected event is emitted + let unlock_amount = 19; + let dummy_smart_contract = MockSmartContract::Wasm(1); + assert_ok!(DappStaking::unbond_and_unstake( + RuntimeOrigin::signed(account), + dummy_smart_contract, + unlock_amount + )); + System::assert_last_event(RuntimeEvent::DappStaking(Event::Unlocking { + account, + amount: unlock_amount, + })); + }) +} + #[test] fn unlock_basic_example_is_ok() { ExtBuilder::build().execute_with(|| { @@ -682,6 +717,29 @@ fn unlock_with_exceeding_unlocking_chunks_storage_limits_fails() { }) } +#[test] +fn withdraw_unbonded_is_ok() { + ExtBuilder::build().execute_with(|| { + // Lock & immediatelly unlock some amount + let account = 2; + let lock_amount = 97; + let unlock_amount = 11; + assert_lock(account, lock_amount); + assert_unlock(account, unlock_amount); + + // Run for enough blocks so the chunk becomes claimable + let unlocking_blocks = DappStaking::unlocking_period(); + run_for_blocks(unlocking_blocks); + assert_ok!(DappStaking::withdraw_unbonded(RuntimeOrigin::signed( + account + ))); + System::assert_last_event(RuntimeEvent::DappStaking(Event::ClaimedUnlocked { + account, + amount: unlock_amount, + })); + }) +} + #[test] fn claim_unlocked_is_ok() { ExtBuilder::build().execute_with(|| { @@ -1792,7 +1850,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 @@ -2204,7 +2262,7 @@ fn force_with_incorrect_origin_fails() { } #[test] -fn get_dapp_tier_assignment_basic_example_works() { +fn get_dapp_tier_assignment_and_rewards_basic_example_works() { ExtBuilder::build().execute_with(|| { // This test will rely on the configuration inside the mock file. // If that changes, this test will have to be updated as well. @@ -2281,7 +2339,7 @@ fn get_dapp_tier_assignment_basic_example_works() { // Finally, the actual test let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; - let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment( + let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment_and_rewards( protocol_state.era + 1, protocol_state.period_number(), dapp_reward_pool, @@ -2299,34 +2357,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 @@ -2344,7 +2393,7 @@ fn get_dapp_tier_assignment_basic_example_works() { } #[test] -fn get_dapp_tier_assignment_zero_slots_per_tier_works() { +fn get_dapp_tier_assignment_and_rewards_zero_slots_per_tier_works() { ExtBuilder::build().execute_with(|| { // This test will rely on the configuration inside the mock file. // If that changes, this test might have to be updated as well. @@ -2359,7 +2408,7 @@ fn get_dapp_tier_assignment_zero_slots_per_tier_works() { // Calculate tier assignment (we don't need dApps for this test) let protocol_state = ActiveProtocolState::::get(); let dapp_reward_pool = 1000000; - let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment( + let (tier_assignment, counter) = DappStaking::get_dapp_tier_assignment_and_rewards( protocol_state.era, protocol_state.period_number(), dapp_reward_pool, diff --git a/pallets/dapp-staking-v3/src/test/tests_types.rs b/pallets/dapp-staking-v3/src/test/tests_types.rs index 8bf9183b69..61c1740b5d 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 cf70f0a6aa..671f5a0e97 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, } @@ -1757,19 +1729,3 @@ pub struct CleanupMarker { #[codec(compact)] pub dapp_tiers_index: EraNumber, } - -/////////////////////////////////////////////////////////////////////// -//////////// MOVE THIS TO SOME PRIMITIVES CRATE LATER //////////// -/////////////////////////////////////////////////////////////////////// - -/// Interface for fetching price of the native token. -/// -/// TODO: discussion about below -/// The assumption is that the underlying implementation will ensure -/// this price is averaged and/or weighted over a certain period of time. -/// Alternative is to provide e.g. number of blocks for which an approximately averaged value is needed, -/// and let the underlying implementation take care converting block range into value best represening it. -pub trait PriceProvider { - /// Get the price of the native token. - fn average_price() -> FixedU64; -} diff --git a/pallets/static-price-provider/Cargo.toml b/pallets/static-price-provider/Cargo.toml new file mode 100644 index 0000000000..5d1251408e --- /dev/null +++ b/pallets/static-price-provider/Cargo.toml @@ -0,0 +1,52 @@ +[package] +name = "pallet-static-price-provider" +version = "0.1.0" +license = "GPL-3.0-or-later" +description = "Static price provider for native currency" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +log = { workspace = true } +parity-scale-codec = { workspace = true } +serde = { workspace = true } + +astar-primitives = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +scale-info = { workspace = true } +sp-arithmetic = { workspace = true } +sp-runtime = { workspace = true } +sp-std = { workspace = true } + +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +pallet-balances = { workspace = true } +sp-core = { workspace = true } + +[features] +default = ["std"] +std = [ + "parity-scale-codec/std", + "log/std", + "sp-core/std", + "scale-info/std", + "serde/std", + "sp-std/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "astar-primitives/std", + "sp-arithmetic/std", +] +runtime-benchmarks = [ + "frame-benchmarking", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", + "astar-primitives/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/static-price-provider/src/lib.rs b/pallets/static-price-provider/src/lib.rs new file mode 100644 index 0000000000..7e1c629e11 --- /dev/null +++ b/pallets/static-price-provider/src/lib.rs @@ -0,0 +1,128 @@ +// 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 . + +//! # Static Price Provider Pallet +//! +//! A simple pallet that provides a static price for the native currency. +//! This is a temporary solution before oracle is implemented & operational. +//! +//! ## Overview +//! +//! The Static Price Provider pallet provides functionality for setting the active native currency price via privileged call. +//! Only the root can set the price. +//! +//! Network maintainers must ensure to update the price at appropriate times so that inflation & dApp Staking rewards are calculated correctly. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{pallet_prelude::*, traits::OnRuntimeUpgrade}; +use frame_system::{ensure_root, pallet_prelude::*}; +pub use pallet::*; +use sp_arithmetic::{fixed_point::FixedU64, traits::Zero, FixedPointNumber}; +use sp_std::marker::PhantomData; + +use astar_primitives::oracle::PriceProvider; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +#[frame_support::pallet] +pub mod pallet { + + use super::*; + + /// The current storage version. + pub const STORAGE_VERSION: StorageVersion = StorageVersion::new(1); + + #[pallet::pallet] + #[pallet::storage_version(STORAGE_VERSION)] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(crate) fn deposit_event)] + pub enum Event { + /// New static native currency price has been set. + PriceSet { price: FixedU64 }, + } + + #[pallet::error] + pub enum Error { + /// Zero is invalid value for the price (hopefully). + ZeroPrice, + } + + /// Default value handler for active price. + /// This pallet is temporary and it's not worth bothering with genesis config. + pub struct DefaultActivePrice; + impl Get for DefaultActivePrice { + fn get() -> FixedU64 { + FixedU64::from_rational(1, 10) + } + } + + /// Current active native currency price. + #[pallet::storage] + #[pallet::whitelist_storage] + pub type ActivePrice = StorageValue<_, FixedU64, ValueQuery, DefaultActivePrice>; + + #[pallet::call] + impl Pallet { + /// Privileged action used to set the active native currency price. + /// + /// This is a temporary solution before oracle is implemented & operational. + #[pallet::call_index(0)] + #[pallet::weight(T::DbWeight::get().writes(1))] + pub fn force_set_price(origin: OriginFor, price: FixedU64) -> DispatchResult { + ensure_root(origin)?; + ensure!(!price.is_zero(), Error::::ZeroPrice); + + ActivePrice::::put(price); + + Self::deposit_event(Event::::PriceSet { price }); + + Ok(().into()) + } + } + + impl PriceProvider for Pallet { + fn average_price() -> FixedU64 { + ActivePrice::::get() + } + } +} + +/// `OnRuntimeUpgrade` logic for integrating this pallet into the live network. +pub struct InitActivePrice(PhantomData<(T, P)>); +impl> OnRuntimeUpgrade for InitActivePrice { + fn on_runtime_upgrade() -> Weight { + let init_price = P::get().max(FixedU64::from_rational(1, FixedU64::DIV.into())); + + log::info!("Setting initial active price to {:?}", init_price); + ActivePrice::::put(init_price); + + T::DbWeight::get().writes(1) + } +} diff --git a/pallets/static-price-provider/src/mock.rs b/pallets/static-price-provider/src/mock.rs new file mode 100644 index 0000000000..99c35b5943 --- /dev/null +++ b/pallets/static-price-provider/src/mock.rs @@ -0,0 +1,117 @@ +// 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 crate::{self as pallet_static_price_provider}; + +use frame_support::{ + construct_runtime, parameter_types, + sp_io::TestExternalities, + traits::{ConstU128, ConstU32}, + weights::Weight, +}; + +use sp_core::H256; +use sp_runtime::traits::{BlakeTwo256, IdentityLookup}; + +use astar_primitives::{testing::Header, Balance, BlockNumber}; +type AccountId = u64; + +type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; +type Block = frame_system::mocking::MockBlock; + +parameter_types! { + pub const BlockHashCount: BlockNumber = 250; + pub BlockWeights: frame_system::limits::BlockWeights = + frame_system::limits::BlockWeights::simple_max(Weight::from_parts(1024, 0)); +} + +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +impl pallet_balances::Config for Test { + type MaxLocks = ConstU32<4>; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type Balance = Balance; + type RuntimeEvent = RuntimeEvent; + type DustRemoval = (); + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = ConstU32<0>; + type MaxFreezes = ConstU32<0>; +} + +impl pallet_static_price_provider::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +construct_runtime!( + pub struct Test + where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + StaticPriceProvider: pallet_static_price_provider, + } +); + +pub struct ExternalityBuilder; +impl ExternalityBuilder { + pub fn build() -> TestExternalities { + let storage = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + + let mut ext = TestExternalities::from(storage); + ext.execute_with(|| { + System::set_block_number(1); + }); + + ext + } +} diff --git a/pallets/static-price-provider/src/tests.rs b/pallets/static-price-provider/src/tests.rs new file mode 100644 index 0000000000..c0d77917d9 --- /dev/null +++ b/pallets/static-price-provider/src/tests.rs @@ -0,0 +1,60 @@ +// 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::{pallet::Error, Event, *}; +use frame_support::{assert_noop, assert_ok}; +use mock::*; +use sp_runtime::traits::{BadOrigin, Zero}; + +#[test] +fn force_set_price_works() { + ExternalityBuilder::build().execute_with(|| { + assert!(!ActivePrice::::get().is_zero(), "Sanity check"); + + let new_price = ActivePrice::::get() * 2.into(); + assert_ok!(StaticPriceProvider::force_set_price( + RuntimeOrigin::root(), + new_price + )); + System::assert_last_event(RuntimeEvent::StaticPriceProvider(Event::PriceSet { + price: new_price, + })); + assert_eq!(ActivePrice::::get(), new_price); + assert_eq!(StaticPriceProvider::average_price(), new_price); + }) +} + +#[test] +fn force_set_zero_price_fails() { + ExternalityBuilder::build().execute_with(|| { + assert_noop!( + StaticPriceProvider::force_set_price(RuntimeOrigin::root(), 0.into()), + Error::::ZeroPrice + ); + }) +} + +#[test] +fn force_set_price_with_invalid_origin_fails() { + ExternalityBuilder::build().execute_with(|| { + assert_noop!( + StaticPriceProvider::force_set_price(RuntimeOrigin::signed(1), 1.into()), + BadOrigin + ); + }) +} diff --git a/precompiles/dapp-staking-v3/DappsStakingV1.sol b/precompiles/dapp-staking-v3/DappsStakingV1.sol deleted file mode 100644 index 9b13429413..0000000000 --- a/precompiles/dapp-staking-v3/DappsStakingV1.sol +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-License-Identifier: BSD-3-Clause - -pragma solidity >=0.8.0; - -/// Predeployed at the address 0x0000000000000000000000000000000000005001 -/// For better understanding check the source code: -/// repo: https://github.com/AstarNetwork/Astar -/// -/// **NOTE:** This is a soft-deprecated interface used by the old dApps staking v2. -/// It is still supported by the network, but doesn't reflect how dApp staking v3 should be used. -/// Please refer to the `v2` interface for the latest version of the dApp staking contract. -/// -/// It is possible that dApp staking feature will once again evolve in the future so all developers are encouraged -/// to keep their smart contracts which utilize dApp staking precompile interface as upgradable, or implement their logic -/// in such a way it's relatively simple to migrate to the new version of the interface. -interface DappsStaking { - - // Types - - /// Instruction how to handle reward payout for staker. - /// `FreeBalance` - Reward will be paid out to the staker (free balance). - /// `StakeBalance` - Reward will be paid out to the staker and is immediately restaked (locked balance) - enum RewardDestination {FreeBalance, StakeBalance} - - // Storage getters - - /// @notice Read current era. - /// @return era: The current era - function read_current_era() external view returns (uint256); - - /// @notice Read the unbonding period (or unlocking period) in the number of eras. - /// @return period: The unbonding period in eras - function read_unbonding_period() external view returns (uint256); - - /// @notice Read Total network reward for the given era - sum of staker & dApp rewards. - /// @return reward: Total network reward for the given era - function read_era_reward(uint32 era) external view returns (uint128); - - /// @notice Read Total staked amount for the given era - /// @return staked: Total staked amount for the given era - function read_era_staked(uint32 era) external view returns (uint128); - - /// @notice Read Staked amount for the staker - /// @param staker: The staker address in form of 20 or 32 hex bytes - /// @return amount: Staked amount by the staker - function read_staked_amount(bytes calldata staker) external view returns (uint128); - - /// @notice Read Staked amount on a given contract for the staker - /// @param contract_id: The smart contract address used for staking - /// @param staker: The staker address in form of 20 or 32 hex bytes - /// @return amount: Staked amount by the staker - function read_staked_amount_on_contract(address contract_id, bytes calldata staker) external view returns (uint128); - - /// @notice Read the staked amount from the era when the amount was last staked/unstaked - /// @return total: The most recent total staked amount on contract - function read_contract_stake(address contract_id) external view returns (uint128); - - - // Extrinsic calls - - /// @notice Register is root origin only and not allowed via evm precompile. - /// This should always fail. - function register(address) external returns (bool); - - /// @notice Stake provided amount on the contract. - function bond_and_stake(address, uint128) external returns (bool); - - /// @notice Start unbonding process and unstake balance from the contract. - function unbond_and_unstake(address, uint128) external returns (bool); - - /// @notice Withdraw all funds that have completed the unbonding process. - function withdraw_unbonded() external returns (bool); - - /// @notice Claim earned staker rewards for the oldest unclaimed era. - /// In order to claim multiple eras, this call has to be called multiple times. - /// Staker account is derived from the caller address. - /// @param smart_contract: The smart contract address used for staking - function claim_staker(address smart_contract) external returns (bool); - - /// @notice Claim one era of unclaimed dapp rewards for the specified contract and era. - /// @param smart_contract: The smart contract address used for staking - /// @param era: The era to be claimed - function claim_dapp(address smart_contract, uint128 era) external returns (bool); - - /// @notice Set reward destination for staker rewards - /// @param reward_destination: The instruction on how the reward payout should be handled - function set_reward_destination(RewardDestination reward_destination) external returns (bool); - - /// @notice Withdraw staked funds from an unregistered contract. - /// @param smart_contract: The smart contract address used for staking - function withdraw_from_unregistered(address smart_contract) external returns (bool); - - /// @notice Transfer part or entire nomination from origin smart contract to target smart contract - /// @param origin_smart_contract: The origin smart contract address - /// @param amount: The amount to transfer from origin to target - /// @param target_smart_contract: The target smart contract address - function nomination_transfer(address origin_smart_contract, uint128 amount, address target_smart_contract) external returns (bool); -} diff --git a/precompiles/dapp-staking-v3/DappsStakingV2.sol b/precompiles/dapp-staking-v3/DappsStakingV2.sol index 40c55af6c4..655b18f582 100644 --- a/precompiles/dapp-staking-v3/DappsStakingV2.sol +++ b/precompiles/dapp-staking-v3/DappsStakingV2.sol @@ -5,88 +5,96 @@ pragma solidity >=0.8.0; /// Predeployed at the address 0x0000000000000000000000000000000000005001 /// For better understanding check the source code: /// repo: https://github.com/AstarNetwork/Astar -/// code: pallets/dapp-staking-v3 -interface DAppStaking { +/// +/// **NOTE:** This is a soft-deprecated interface used by the old dApps staking v2. +/// It is still supported by the network, but doesn't reflect how dApp staking v3 should be used. +/// Please refer to the `v3` interface for the latest version of the dApp staking precompile. +/// +/// It is possible that dApp staking feature will once again evolve in the future so all developers are encouraged +/// to keep their smart contracts which utilize dApp staking precompile interface as upgradable, or implement their logic +/// in such a way it's relatively simple to migrate to the new version of the interface. +interface DappsStaking { // Types - /// Describes the subperiod in which the protocol currently is. - enum Subperiod {Voting, BuildAndEarn} - - /// Describes current smart contract types supported by the network. - enum SmartContractType {EVM, WASM} - - /// @notice Describes protocol state. - /// @param era: Ongoing era number. - /// @param period: Ongoing period number. - /// @param subperiod: Ongoing subperiod type. - struct ProtocolState { - uint256 era; - uint256 period; - Subperiod subperiod; - } - - /// @notice Used to describe smart contract. Astar supports both EVM & WASM smart contracts - /// so it's important to differentiate between the two. This approach also allows - /// easy extensibility in the future. - /// @param contract_type: Type of the smart contract to be used - struct SmartContract { - SmartContractType contract_type; - bytes contract_address; - } + /// Instruction how to handle reward payout for staker. + /// `FreeBalance` - Reward will be paid out to the staker (free balance). + /// `StakeBalance` - Reward will be paid out to the staker and is immediately restaked (locked balance) + enum RewardDestination {FreeBalance, StakeBalance} // Storage getters - /// @notice Get the current protocol state. - /// @return (current era, current period number, current subperiod type). - function protocol_state() external view returns (ProtocolState memory); + /// @notice Read current era. + /// @return era: The current era + function read_current_era() external view returns (uint256); - /// @notice Get the unlocking period expressed in the number of blocks. - /// @return period: The unlocking period expressed in the number of blocks. - function unlocking_period() external view returns (uint256); + /// @notice Read the unbonding period (or unlocking period) in the number of eras. + /// @return period: The unbonding period in eras + function read_unbonding_period() external view returns (uint256); + /// @notice Read Total network reward for the given era - sum of staker & dApp rewards. + /// @return reward: Total network reward for the given era + function read_era_reward(uint32 era) external view returns (uint128); - // Extrinsic calls - - /// @notice Lock the given amount of tokens into dApp staking protocol. - /// @param amount: The amount of tokens to be locked. - function lock(uint128 amount) external returns (bool); - - /// @notice Start the unlocking process for the given amount of tokens. - /// @param amount: The amount of tokens to be unlocked. - function unlock(uint128 amount) external returns (bool); + /// @notice Read Total staked amount for the given era + /// @return staked: Total staked amount for the given era + function read_era_staked(uint32 era) external view returns (uint128); - /// @notice Claims unlocked tokens, if there are any - function claim_unlocked() external returns (bool); + /// @notice Read Staked amount for the staker + /// @param staker: The staker address in form of 20 or 32 hex bytes + /// @return amount: Staked amount by the staker + function read_staked_amount(bytes calldata staker) external view returns (uint128); - /// @notice Stake the given amount of tokens on the specified smart contract. - /// The amount specified must be precise, otherwise the call will fail. - /// @param smart_contract: The smart contract to be staked on. - /// @param amount: The amount of tokens to be staked. - function stake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + /// @notice Read Staked amount on a given contract for the staker + /// @param contract_id: The smart contract address used for staking + /// @param staker: The staker address in form of 20 or 32 hex bytes + /// @return amount: Staked amount by the staker + function read_staked_amount_on_contract(address contract_id, bytes calldata staker) external view returns (uint128); - /// @notice Unstake the given amount of tokens from the specified smart contract. - /// The amount specified must be precise, otherwise the call will fail. - /// @param smart_contract: The smart contract to be unstaked from. - /// @param amount: The amount of tokens to be unstaked. - function unstake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + /// @notice Read the staked amount from the era when the amount was last staked/unstaked + /// @return total: The most recent total staked amount on contract + function read_contract_stake(address contract_id) external view returns (uint128); - /// @notice Claims one or more pending staker rewards. - function claim_staker_rewards() external returns (bool); - /// @notice Claim the bonus reward for the specified smart contract. - /// @param smart_contract: The smart contract for which the bonus reward should be claimed. - function claim_bonus_reward(SmartContract calldata smart_contract) external returns (bool); - - /// @notice Claim dApp reward for the specified smart contract & era. - /// @param smart_contract: The smart contract for which the dApp reward should be claimed. - /// @param era: The era for which the dApp reward should be claimed. - function claim_dapp_reward(SmartContract calldata smart_contract, uint256 era) external returns (bool); - - /// @notice Unstake all funds from the unregistered smart contract. - /// @param smart_contract: The smart contract which was unregistered and from which all funds should be unstaked. - function unstake_from_unregistered(SmartContract calldata smart_contract) external returns (bool); + // Extrinsic calls - /// @notice Used to cleanup all expired contract stake entries from the caller. - function cleanup_expired_entries() external returns (bool); + /// @notice Register is root origin only and not allowed via evm precompile. + /// This should always fail. + function register(address) external returns (bool); + + /// @notice Stake provided amount on the contract. + /// If the staker has sufficient locked amount to cover the stake, no additional amount is locked. + /// If the staker has insufficient locked amount to cover the stake, an attempt will be made to lock the missing amount. + function bond_and_stake(address, uint128) external returns (bool); + + /// @notice Unstake balance from the smart contract, and begin the unlocking process. + function unbond_and_unstake(address, uint128) external returns (bool); + + /// @notice Withdraw all funds that have completed the unbonding process. + function withdraw_unbonded() external returns (bool); + + /// @notice Claim earned staker rewards for the oldest unclaimed era. + /// In order to claim multiple eras, this call has to be called multiple times. + /// Staker account is derived from the caller address. + /// @param smart_contract: The smart contract address used for staking + function claim_staker(address smart_contract) external returns (bool); + + /// @notice Claim one era of unclaimed dapp rewards for the specified contract and era. + /// @param smart_contract: The smart contract address used for staking + /// @param era: The era to be claimed + function claim_dapp(address smart_contract, uint128 era) external returns (bool); + + /// @notice This call is deprecated and will always fail. + /// @param reward_destination: The instruction on how the reward payout should be handled (ignored) + function set_reward_destination(RewardDestination reward_destination) external returns (bool); + + /// @notice Unstake funds from an unregistered contract. + /// @param smart_contract: The smart contract address used for staking + function withdraw_from_unregistered(address smart_contract) external returns (bool); + + /// @notice Transfer part or entire nomination from origin smart contract to target smart contract + /// @param origin_smart_contract: The origin smart contract address + /// @param amount: The amount to transfer from origin to target, must be precise. + /// @param target_smart_contract: The target smart contract address + function nomination_transfer(address origin_smart_contract, uint128 amount, address target_smart_contract) external returns (bool); } diff --git a/precompiles/dapp-staking-v3/DappsStakingV3.sol b/precompiles/dapp-staking-v3/DappsStakingV3.sol new file mode 100644 index 0000000000..40c55af6c4 --- /dev/null +++ b/precompiles/dapp-staking-v3/DappsStakingV3.sol @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: BSD-3-Clause + +pragma solidity >=0.8.0; + +/// Predeployed at the address 0x0000000000000000000000000000000000005001 +/// For better understanding check the source code: +/// repo: https://github.com/AstarNetwork/Astar +/// code: pallets/dapp-staking-v3 +interface DAppStaking { + + // Types + + /// Describes the subperiod in which the protocol currently is. + enum Subperiod {Voting, BuildAndEarn} + + /// Describes current smart contract types supported by the network. + enum SmartContractType {EVM, WASM} + + /// @notice Describes protocol state. + /// @param era: Ongoing era number. + /// @param period: Ongoing period number. + /// @param subperiod: Ongoing subperiod type. + struct ProtocolState { + uint256 era; + uint256 period; + Subperiod subperiod; + } + + /// @notice Used to describe smart contract. Astar supports both EVM & WASM smart contracts + /// so it's important to differentiate between the two. This approach also allows + /// easy extensibility in the future. + /// @param contract_type: Type of the smart contract to be used + struct SmartContract { + SmartContractType contract_type; + bytes contract_address; + } + + // Storage getters + + /// @notice Get the current protocol state. + /// @return (current era, current period number, current subperiod type). + function protocol_state() external view returns (ProtocolState memory); + + /// @notice Get the unlocking period expressed in the number of blocks. + /// @return period: The unlocking period expressed in the number of blocks. + function unlocking_period() external view returns (uint256); + + + // Extrinsic calls + + /// @notice Lock the given amount of tokens into dApp staking protocol. + /// @param amount: The amount of tokens to be locked. + function lock(uint128 amount) external returns (bool); + + /// @notice Start the unlocking process for the given amount of tokens. + /// @param amount: The amount of tokens to be unlocked. + function unlock(uint128 amount) external returns (bool); + + /// @notice Claims unlocked tokens, if there are any + function claim_unlocked() external returns (bool); + + /// @notice Stake the given amount of tokens on the specified smart contract. + /// The amount specified must be precise, otherwise the call will fail. + /// @param smart_contract: The smart contract to be staked on. + /// @param amount: The amount of tokens to be staked. + function stake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + + /// @notice Unstake the given amount of tokens from the specified smart contract. + /// The amount specified must be precise, otherwise the call will fail. + /// @param smart_contract: The smart contract to be unstaked from. + /// @param amount: The amount of tokens to be unstaked. + function unstake(SmartContract calldata smart_contract, uint128 amount) external returns (bool); + + /// @notice Claims one or more pending staker rewards. + function claim_staker_rewards() external returns (bool); + + /// @notice Claim the bonus reward for the specified smart contract. + /// @param smart_contract: The smart contract for which the bonus reward should be claimed. + function claim_bonus_reward(SmartContract calldata smart_contract) external returns (bool); + + /// @notice Claim dApp reward for the specified smart contract & era. + /// @param smart_contract: The smart contract for which the dApp reward should be claimed. + /// @param era: The era for which the dApp reward should be claimed. + function claim_dapp_reward(SmartContract calldata smart_contract, uint256 era) external returns (bool); + + /// @notice Unstake all funds from the unregistered smart contract. + /// @param smart_contract: The smart contract which was unregistered and from which all funds should be unstaked. + function unstake_from_unregistered(SmartContract calldata smart_contract) external returns (bool); + + /// @notice Used to cleanup all expired contract stake entries from the caller. + function cleanup_expired_entries() external returns (bool); +} diff --git a/precompiles/dapp-staking-v3/README.md b/precompiles/dapp-staking-v3/README.md new file mode 100644 index 0000000000..98f219548a --- /dev/null +++ b/precompiles/dapp-staking-v3/README.md @@ -0,0 +1,36 @@ +# dApp Staking Precompile Interface + +dApp Staking is at the core of Astar Network, an unique protocol used to incentivize builders to build +great dApps on Astar. + +In order to improve it, the feature has undergone several overhauls in past. +This is necessary to keep Astar competitive, and it's likely it will happen again in the future. + +Developers should account for this when developing dApps. +Even though the interface compatibility will be kept as much as possible, +as feature is changed & evolved, new functionality will become available, and +interface will have to be expanded. + +The logic controlling dApp staking should be upgradable therefore. + +## V2 Interface + +**ONLY RELEVANT FOR DEVELOPERS WHO USED THE OLD INTERFACE.** + +Covers the _so-called_ `dApps staking v2` interface. + +Many actions that are done here as a part of a single call are broken down into multiple calls in the `v3` interface. +Best effort is made to keep the new behavior as compatible as possible with the old behavior. +Regardless, in some cases it's simply not possible. + +Some examples of this: +* Since all stakes are reset at the end of each period, developers will need to adapt their smart contract logic for this. +* Bonus rewards concept was only introduced from the precompile v3 interface, so there's no equivalent call in v2 interface. +* Composite actions like `bond_and_stake`, `unbond_and_unstake` and `nomination_transfer` are implemented as a series of calls to mimic the old logic. +* Claiming staker rewards is detached from a specific staked dApp (or smart contract), and can result in more than 1 era reward being claimed. +* Periods & subperiods concept only exists from the v3 interface. + +## V3 Interface + +Contains functions that _mimic_ the interface of the latest `dApp Staking v3`. +Developers are encouraged to use this interface to fully utilize dApp staking functionality. \ No newline at end of file diff --git a/precompiles/dapp-staking-v3/src/test/mock.rs b/precompiles/dapp-staking-v3/src/test/mock.rs index 2015ba6736..feff4d634f 100644 --- a/precompiles/dapp-staking-v3/src/test/mock.rs +++ b/precompiles/dapp-staking-v3/src/test/mock.rs @@ -39,10 +39,11 @@ extern crate alloc; use astar_primitives::{ dapp_staking::{CycleConfiguration, SmartContract, StakingRewardHandler}, + oracle::PriceProvider, testing::Header, AccountId, Balance, BlockNumber, }; -use pallet_dapp_staking_v3::{EraNumber, PeriodNumber, PriceProvider, TierThreshold}; +use pallet_dapp_staking_v3::{EraNumber, PeriodNumber, TierThreshold}; type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; type Block = frame_system::mocking::MockBlock; diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 737ff4306a..0cbe00078a 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -21,6 +21,7 @@ fp-evm = { workspace = true } # Substrate dependencies frame-support = { workspace = true } pallet-assets = { workspace = true } +sp-arithmetic = { workspace = true } sp-core = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } @@ -66,5 +67,6 @@ std = [ "pallet-evm/std", "pallet-evm-precompile-assets-erc20/std", "pallet-evm-precompile-dispatch/std", + "sp-arithmetic/std", ] runtime-benchmarks = ["xcm-builder/runtime-benchmarks", "pallet-assets/runtime-benchmarks"] diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index a531b87334..bca98006df 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -41,6 +41,9 @@ pub mod dapp_staking; /// Useful primitives for testing. pub mod testing; +/// Oracle & price primitives. +pub mod oracle; + /// Benchmark primitives #[cfg(feature = "runtime-benchmarks")] pub mod benchmarks; diff --git a/primitives/src/oracle.rs b/primitives/src/oracle.rs new file mode 100644 index 0000000000..aa12e690e7 --- /dev/null +++ b/primitives/src/oracle.rs @@ -0,0 +1,28 @@ +// 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 sp_arithmetic::fixed_point::FixedU64; + +/// Interface for fetching price of the native token. +/// +/// **NOTE:** This is just a temporary interface, and will be replaced with a proper oracle which will average +/// the price over a certain period of time. +pub trait PriceProvider { + /// Get the price of the native token. + fn average_price() -> FixedU64; +} diff --git a/runtime/local/Cargo.toml b/runtime/local/Cargo.toml index 231cda0aab..14967b8a6c 100644 --- a/runtime/local/Cargo.toml +++ b/runtime/local/Cargo.toml @@ -81,6 +81,7 @@ pallet-evm-precompile-substrate-ecdsa = { workspace = true } pallet-evm-precompile-unified-accounts = { workspace = true } pallet-evm-precompile-xvm = { workspace = true } pallet-inflation = { workspace = true } +pallet-static-price-provider = { workspace = true } pallet-unified-accounts = { workspace = true } pallet-xvm = { workspace = true } @@ -130,6 +131,7 @@ std = [ "pallet-dapp-staking-migration/std", "dapp-staking-v3-runtime-api/std", "pallet-inflation/std", + "pallet-static-price-provider/std", "pallet-dynamic-evm-base-fee/std", "pallet-ethereum/std", "pallet-evm/std", @@ -241,6 +243,7 @@ try-runtime = [ "pallet-evm/try-runtime", "pallet-ethereum-checked/try-runtime", "pallet-dapp-staking-migration/try-runtime", + "pallet-static-price-provider/try-runtime", ] evm-tracing = [ "moonbeam-evm-tracer", diff --git a/runtime/local/src/lib.rs b/runtime/local/src/lib.rs index b1bec5ead9..87be4ef2de 100644 --- a/runtime/local/src/lib.rs +++ b/runtime/local/src/lib.rs @@ -48,7 +48,6 @@ use pallet_grandpa::{fg_primitives, AuthorityList as GrandpaAuthorityList}; use pallet_transaction_payment::{CurrencyAdapter, Multiplier, TargetedFeeAdjustment}; use parity_scale_codec::{Compact, Decode, Encode, MaxEncodedLen}; use sp_api::impl_runtime_apis; -use sp_arithmetic::fixed_point::FixedU64; use sp_core::{crypto::KeyTypeId, ConstBool, OpaqueMetadata, H160, H256, U256}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, @@ -59,7 +58,7 @@ use sp_runtime::{ transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError}, ApplyExtrinsicResult, FixedPointNumber, Perbill, Permill, Perquintill, RuntimeDebug, }; -use sp_std::prelude::*; +use sp_std::{collections::btree_map::BTreeMap, prelude::*}; use astar_primitives::{ dapp_staking::{CycleConfiguration, SmartContract}, @@ -485,11 +484,8 @@ impl pallet_dapps_staking::Config for Runtime { type ForcePalletDisabled = ConstBool; // This will be set to `true` when needed } -pub struct DummyPriceProvider; -impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { - fn average_price() -> FixedU64 { - FixedU64::from_rational(1, 10) - } +impl pallet_static_price_provider::Config for Runtime { + type RuntimeEvent = RuntimeEvent; } #[cfg(feature = "runtime-benchmarks")] @@ -515,7 +511,7 @@ impl pallet_dapp_staking_v3::Config for Runtime { type Currency = Balances; type SmartContract = SmartContract; type ManagerOrigin = frame_system::EnsureRoot; - type NativePriceProvider = DummyPriceProvider; + type NativePriceProvider = StaticPriceProvider; type StakingRewardHandler = Inflation; type CycleConfiguration = InflationCycleConfig; type EraRewardSpanLength = ConstU32<8>; @@ -932,7 +928,7 @@ impl pallet_contracts::Config for Runtime { type AddressGenerator = pallet_contracts::DefaultAddressGenerator; type MaxCodeLen = ConstU32<{ 123 * 1024 }>; type MaxStorageKeyLen = ConstU32<128>; - type UnsafeUnstableInterface = ConstBool; + type UnsafeUnstableInterface = ConstBool; type MaxDebugBufferLen = ConstU32<{ 2 * 1024 * 1024 }>; } @@ -1104,6 +1100,7 @@ construct_runtime!( DappStaking: pallet_dapp_staking_v3, DappStakingMigration: pallet_dapp_staking_migration, Inflation: pallet_inflation, + StaticPriceProvider: pallet_static_price_provider, BlockReward: pallet_block_rewards_hybrid, TransactionPayment: pallet_transaction_payment, EVM: pallet_evm, @@ -1731,6 +1728,10 @@ impl_runtime_apis! { } impl dapp_staking_v3_runtime_api::DappStakingApi for Runtime { + fn periods_per_cycle() -> pallet_dapp_staking_v3::PeriodNumber { + InflationCycleConfig::periods_per_cycle() + } + fn eras_per_voting_subperiod() -> pallet_dapp_staking_v3::EraNumber { InflationCycleConfig::eras_per_voting_subperiod() } @@ -1742,6 +1743,10 @@ impl_runtime_apis! { fn blocks_per_era() -> BlockNumber { InflationCycleConfig::blocks_per_era() } + + fn get_dapp_tier_assignment() -> BTreeMap { + DappStaking::get_dapp_tier_assignment() + } } #[cfg(feature = "runtime-benchmarks")] diff --git a/runtime/shibuya/Cargo.toml b/runtime/shibuya/Cargo.toml index b79902de3b..1996fab9fa 100644 --- a/runtime/shibuya/Cargo.toml +++ b/runtime/shibuya/Cargo.toml @@ -114,6 +114,7 @@ pallet-evm-precompile-unified-accounts = { workspace = true } pallet-evm-precompile-xcm = { workspace = true } pallet-evm-precompile-xvm = { workspace = true } pallet-inflation = { workspace = true } +pallet-static-price-provider = { workspace = true } pallet-unified-accounts = { workspace = true } pallet-xc-asset-config = { workspace = true } pallet-xcm = { workspace = true } @@ -197,6 +198,7 @@ std = [ "pallet-dapp-staking-migration/std", "dapp-staking-v3-runtime-api/std", "pallet-inflation/std", + "pallet-static-price-provider/std", "pallet-identity/std", "pallet-multisig/std", "pallet-insecure-randomness-collective-flip/std", @@ -288,6 +290,7 @@ try-runtime = [ "pallet-dapp-staking-v3/try-runtime", "pallet-dapp-staking-migration/try-runtime", "pallet-inflation/try-runtime", + "pallet-static-price-provider/try-runtime", "pallet-sudo/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", diff --git a/runtime/shibuya/src/lib.rs b/runtime/shibuya/src/lib.rs index 905ffdea5c..44d6c2c13b 100644 --- a/runtime/shibuya/src/lib.rs +++ b/runtime/shibuya/src/lib.rs @@ -65,7 +65,7 @@ use sp_runtime::{ transaction_validity::{TransactionSource, TransactionValidity, TransactionValidityError}, ApplyExtrinsicResult, FixedPointNumber, Perbill, Permill, Perquintill, RuntimeDebug, }; -use sp_std::prelude::*; +use sp_std::{collections::btree_map::BTreeMap, prelude::*}; use astar_primitives::{ dapp_staking::{CycleConfiguration, SmartContract}, @@ -413,13 +413,8 @@ impl pallet_dapps_staking::Config for Runtime { type ForcePalletDisabled = ConstBool; } -// Placeholder until we introduce a pallet for this. -// Real solution will be an oracle. -pub struct DummyPriceProvider; -impl pallet_dapp_staking_v3::PriceProvider for DummyPriceProvider { - fn average_price() -> FixedU64 { - FixedU64::from_rational(1, 10) - } +impl pallet_static_price_provider::Config for Runtime { + type RuntimeEvent = RuntimeEvent; } #[cfg(feature = "runtime-benchmarks")] @@ -449,7 +444,7 @@ impl pallet_dapp_staking_v3::Config for Runtime { type Currency = Balances; type SmartContract = SmartContract; type ManagerOrigin = frame_system::EnsureRoot; - type NativePriceProvider = DummyPriceProvider; + type NativePriceProvider = StaticPriceProvider; type StakingRewardHandler = Inflation; type CycleConfiguration = InflationCycleConfig; type EraRewardSpanLength = ConstU32<16>; @@ -1336,6 +1331,8 @@ construct_runtime!( Sudo: pallet_sudo = 99, + // To be removed & cleaned up once proper oracle is implemented + StaticPriceProvider: pallet_static_price_provider = 253, // To be removed & cleaned up after migration has been finished DappStakingMigration: pallet_dapp_staking_migration = 254, // Legacy dApps staking v2, to be removed after migration has been finished @@ -1380,7 +1377,16 @@ 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 struct InitActivePriceGet; +impl Get for InitActivePriceGet { + fn get() -> FixedU64 { + FixedU64::from_rational(1, 10) + } +} +pub type Migrations = ( + pallet_static_price_provider::InitActivePrice, + pallet_dapp_staking_v3::migrations::DappStakingV3TierRewardAsTree, +); type EventRecord = frame_system::EventRecord< ::RuntimeEvent, @@ -1928,6 +1934,10 @@ impl_runtime_apis! { } impl dapp_staking_v3_runtime_api::DappStakingApi for Runtime { + fn periods_per_cycle() -> pallet_dapp_staking_v3::PeriodNumber { + InflationCycleConfig::periods_per_cycle() + } + fn eras_per_voting_subperiod() -> pallet_dapp_staking_v3::EraNumber { InflationCycleConfig::eras_per_voting_subperiod() } @@ -1939,6 +1949,10 @@ impl_runtime_apis! { fn blocks_per_era() -> BlockNumber { InflationCycleConfig::blocks_per_era() } + + fn get_dapp_tier_assignment() -> BTreeMap { + DappStaking::get_dapp_tier_assignment() + } } #[cfg(feature = "runtime-benchmarks")]