diff --git a/pallets/economy/src/lib.rs b/pallets/economy/src/lib.rs index 30060b94..0439e343 100644 --- a/pallets/economy/src/lib.rs +++ b/pallets/economy/src/lib.rs @@ -18,6 +18,8 @@ #![cfg_attr(not(feature = "std"), no_std)] use codec::{Encode, HasCompact}; +use frame_benchmarking::log; +use frame_support::traits::ExistenceRequirement; use frame_support::{ ensure, pallet_prelude::*, @@ -26,17 +28,21 @@ use frame_support::{ }; use frame_system::{ensure_signed, pallet_prelude::*}; use orml_traits::{DataProvider, MultiCurrency, MultiReservableCurrency}; -use sp_runtime::traits::{BlockNumberProvider, CheckedAdd, CheckedMul, Saturating}; +use sp_core::U256; +use sp_runtime::traits::{ + BlockNumberProvider, CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Saturating, UniqueSaturatedInto, +}; use sp_runtime::{ traits::{AccountIdConversion, One, Zero}, - ArithmeticError, DispatchError, Perbill, + ArithmeticError, DispatchError, FixedPointNumber, Perbill, SaturatedConversion, }; use sp_std::{collections::btree_map::BTreeMap, prelude::*, vec::Vec}; use core_primitives::NFTTrait; use core_primitives::*; pub use pallet::*; -use primitives::{estate::Estate, EstateId}; +use primitives::bounded::Rate; +use primitives::{estate::Estate, EraIndex, EstateId, PoolId, StakingRound}; use primitives::{Balance, ClassId, DomainId, FungibleTokenId, PowerAmount, RoundIndex}; pub use weights::WeightInfo; @@ -73,9 +79,11 @@ pub mod weights; #[frame_support::pallet] pub mod pallet { + use frame_benchmarking::log; use sp_runtime::traits::{CheckedAdd, CheckedSub, Saturating}; use sp_runtime::ArithmeticError; + use primitives::bounded::{FractionalRate, Rate}; use primitives::{staking::Bond, ClassId, CurrencyId, NftId, PoolId}; use super::*; @@ -102,7 +110,7 @@ pub mod pallet { type FungibleTokenCurrency: MultiReservableCurrency< Self::AccountId, CurrencyId = FungibleTokenId, - Balance = Balance, + Balance = BalanceOf, >; /// NFT handler @@ -134,6 +142,9 @@ pub mod pallet { #[pallet::constant] type PowerAmountPerBlock: Get; + // Reward payout account + #[pallet::constant] + type RewardPayoutAccount: Get; /// Weight info type WeightInfo: WeightInfo; } @@ -217,12 +228,48 @@ pub mod pallet { /// Record reward pool info. /// - /// map PoolId => PoolInfo + /// StakingRewardPoolInfo #[pallet::storage] #[pallet::getter(fn staking_reward_pool_info)] pub type StakingRewardPoolInfo = StorageValue<_, InnovationStakingPoolInfo, BalanceOf, FungibleTokenId>, ValueQuery>; + /// Self-staking exit queue info + /// This will keep track of stake exits queue, unstake only allows after 1 round + #[pallet::storage] + #[pallet::getter(fn innovation_staking_exit_queue)] + pub type InnovationStakingExitQueue = + StorageDoubleMap<_, Blake2_128Concat, T::AccountId, Twox64Concat, RoundIndex, BalanceOf, OptionQuery>; + + /// The pending rewards amount accumulated from staking on innovation, pending reward added when + /// user claim reward or remove shares + /// + /// PendingRewards: map AccountId => BTreeMap + #[pallet::storage] + #[pallet::getter(fn pending_multi_rewards)] + pub type PendingRewardsOfStakingInnovation = + StorageMap<_, Twox64Concat, T::AccountId, BTreeMap>, ValueQuery>; + + /// The current era index + #[pallet::storage] + #[pallet::getter(fn current_era)] + pub type CurrentEra = StorageValue<_, EraIndex, ValueQuery>; + + /// The block number of last era updated + #[pallet::storage] + #[pallet::getter(fn last_era_updated_block)] + pub type LastEraUpdatedBlock = StorageValue<_, BlockNumberFor, ValueQuery>; + + /// The internal of block number between era. + #[pallet::storage] + #[pallet::getter(fn update_era_frequency)] + pub type UpdateEraFrequency = StorageValue<_, BlockNumberFor, ValueQuery>; + + /// The estimated staking reward rate per era on innovation staking. + /// + /// EstimatedStakingRewardRatePerEra: value: Rate + #[pallet::storage] + pub type EstimatedStakingRewardPerEra = StorageValue<_, BalanceOf, ValueQuery>; #[pallet::event] #[pallet::generate_deposit(pub (super) fn deposit_event)] pub enum Event { @@ -246,6 +293,18 @@ pub mod pallet { CancelPowerConversionRequest((ClassId, TokenId), T::AccountId), /// Innovation Staking [staker, amount] StakedInnovation(T::AccountId, BalanceOf), + /// Unstaked from Innovation [staker, amount] + UnstakedInnovation(T::AccountId, BalanceOf), + /// Claim rewards + ClaimRewards(T::AccountId, FungibleTokenId, BalanceOf), + /// Current innovation staking era updated + CurrentInnovationStakingEraUpdated(EraIndex), + /// Innovation Staking Era frequency updated + UpdatedInnovationStakingEraFrequency(BlockNumberFor), + /// Last innovation staking era updated + LastInnovationStakingEraUpdated(BlockNumberFor), + /// Estimated reward per era + EstimatedRewardPerEraUpdated(BalanceOf), } #[pallet::error] @@ -298,55 +357,31 @@ pub mod pallet { EstateExitQueueDoesNotExit, /// Stake amount exceed estate max amount StakeAmountExceedMaximumAmount, + /// Invalid era set up config + InvalidLastEraUpdatedBlock, + /// Unexpected error + Unexpected, + /// Reward pool does not exist + RewardPoolDoesNotExist, + /// Invalid reward set up + InvalidEstimatedRewardSetup, } - #[pallet::call] - impl Pallet { - /// Set bit power exchange rate - /// - /// The dispatch origin for this call must be _Root_. - /// - /// `rate`: exchange rate of bit to power. input is BIT price per power - /// - /// Emit `BitPowerExchangeRateUpdated` event if successful - #[pallet::weight(T::WeightInfo::set_bit_power_exchange_rate())] - #[transactional] - pub fn set_bit_power_exchange_rate(origin: OriginFor, rate: Balance) -> DispatchResultWithPostInfo { - // Only root can authorize - ensure_root(origin)?; - - BitPowerExchangeRate::::set(rate); - - Self::deposit_event(Event::::BitPowerExchangeRateUpdated(rate)); - - Ok(().into()) - } - - /// Set power balance for specific NFT - /// - /// The dispatch origin for this call must be _Root_. - /// - /// `beneficiary`: NFT account that receives power - /// `amount`: amount of power - /// - /// Emit `SetPowerBalance` event if successful - #[pallet::weight(T::WeightInfo::set_power_balance())] - #[transactional] - pub fn set_power_balance( - origin: OriginFor, - beneficiary: (ClassId, TokenId), - amount: PowerAmount, - ) -> DispatchResultWithPostInfo { - ensure_root(origin)?; - - let account_id = T::EconomyTreasury::get().into_sub_account_truncating(beneficiary); - PowerBalance::::insert(&account_id, amount); + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_initialize(_n: BlockNumberFor) -> Weight { + let era_number = Self::get_era_index(>::block_number()); - Self::deposit_event(Event::::SetPowerBalance(account_id, amount)); + if !era_number.is_zero() { + let _ = Self::update_current_era(era_number).map_err(|err| err).ok(); + } - Ok(().into()) + T::WeightInfo::stake_b() } + } + #[pallet::call] + impl Pallet { /// Stake native token to staking ledger to receive build material every round /// /// The dispatch origin for this call must be _Signed_. @@ -468,17 +503,17 @@ pub mod pallet { /// Emit `SelfStakedToEconomy101` event or `EstateStakedToEconomy101` event if successful #[pallet::weight(T::WeightInfo::stake_a())] #[transactional] - pub fn stake_on_innovation(origin: OriginFor, add_amount: BalanceOf) -> DispatchResult { + pub fn stake_on_innovation(origin: OriginFor, amount: BalanceOf) -> DispatchResult { let who = ensure_signed(origin)?; // Check if user has enough balance for staking ensure!( - T::Currency::free_balance(&who) >= add_amount, + T::Currency::free_balance(&who) >= amount, Error::::InsufficientBalanceForStaking ); ensure!( - !add_amount.is_zero() || add_amount >= T::MinimumStake::get(), + !amount.is_zero() || amount >= T::MinimumStake::get(), Error::::StakeBelowMinimum ); @@ -486,65 +521,129 @@ pub mod pallet { // Check if user already in exit queue ensure!( - !ExitQueue::::contains_key(&who, current_round.current), + !InnovationStakingExitQueue::::contains_key(&who, current_round.current), Error::::ExitQueueAlreadyScheduled ); let staked_balance = InnovationStakingInfo::::get(&who); - let total = staked_balance - .checked_add(&add_amount) - .ok_or(ArithmeticError::Overflow)?; + let total = staked_balance.checked_add(&amount).ok_or(ArithmeticError::Overflow)?; ensure!(total >= T::MinimumStake::get(), Error::::StakeBelowMinimum); - T::Currency::reserve(&who, add_amount)?; + T::Currency::reserve(&who, amount)?; InnovationStakingInfo::::insert(&who, total); - let new_total_staked = TotalInnovationStaking::::get().saturating_add(add_amount); + let new_total_staked = TotalInnovationStaking::::get().saturating_add(amount); >::put(new_total_staked); - StakingRewardPoolInfo::::mutate(|pool_info| { - let initial_total_shares = pool_info.total_shares; - pool_info.total_shares = pool_info.total_shares.saturating_add(add_amount); - let mut withdrawn_inflation = Vec::<(FungibleTokenId, BalanceOf)>::new(); - pool_info - .rewards - .iter_mut() - .for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| { - let reward_inflation = if initial_total_shares.is_zero() { - Zero::zero() - } else { - U256::from(add_amount.to_owned().saturated_into::()) - .saturating_mul(total_reward.to_owned().saturated_into::().into()) - .checked_div(initial_total_shares.to_owned().saturated_into::().into()) - .unwrap_or_default() - .as_u128() - .saturated_into() - }; - *total_reward = total_reward.saturating_add(reward_inflation); - *total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_inflation); + Self::add_share(&who, amount); - withdrawn_inflation.push((*reward_currency, reward_inflation)); - }); + Self::deposit_event(Event::StakedInnovation(who, amount)); - SharesAndWithdrawnRewards::::mutate(who, |(share, withdrawn_rewards)| { - *share = share.saturating_add(add_amount); - // update withdrawn inflation for each reward currency - withdrawn_inflation - .into_iter() - .for_each(|(reward_currency, reward_inflation)| { - withdrawn_rewards - .entry(reward_currency) - .and_modify(|withdrawn_reward| { - *withdrawn_reward = withdrawn_reward.saturating_add(reward_inflation); - }) - .or_insert(reward_inflation); - }); - }); - }); + Ok(()) + } - Self::deposit_event(Event::StakedInnovation(who, amount)); + /// Unstake native token to innovation staking ledger to receive reward and voting points + /// every round + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// `amount`: the unstake amount + /// + /// Emit `UnstakedInnovation` event if successful + #[pallet::weight(T::WeightInfo::stake_a())] + #[transactional] + pub fn unstake_on_innovation(origin: OriginFor, amount: BalanceOf) -> DispatchResult { + let who = ensure_signed(origin)?; + + let staked_balance = InnovationStakingInfo::::get(&who); + ensure!(amount <= staked_balance, Error::::UnstakeAmountExceedStakedAmount); + + let remaining = staked_balance.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?; + + let amount_to_unstake = if remaining < T::MinimumStake::get() { + // Remaining amount below minimum, remove all staked amount + staked_balance + } else { + amount + }; + + let current_round = T::RoundHandler::get_current_round_info(); + let next_round = current_round.current.saturating_add(28u32); + + // Check if user already in exit queue of the current + ensure!( + !InnovationStakingExitQueue::::contains_key(&who, next_round), + Error::::ExitQueueAlreadyScheduled + ); + + // This exit queue will be executed by exit_staking extrinsics to unreserved token + InnovationStakingExitQueue::::insert(&who, next_round.clone(), amount_to_unstake); + + // Update staking info of user immediately + // Remove staking info + if amount_to_unstake == staked_balance { + InnovationStakingInfo::::remove(&who); + } else { + InnovationStakingInfo::::insert(&who, remaining); + } + + let new_total_staked = TotalInnovationStaking::::get().saturating_sub(amount_to_unstake); + >::put(new_total_staked); + + Self::remove_share(&who, amount_to_unstake); + + Self::deposit_event(Event::UnstakedInnovation(who, amount)); + Ok(()) + } + + /// Claim reward from innovation staking ledger to receive reward and voting points + /// every round + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// `amount`: the unstake amount + /// + /// Emit `UnstakedInnovation` event if successful + #[pallet::weight(T::WeightInfo::stake_a())] + #[transactional] + pub fn claim_reward(origin: OriginFor) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::claim_rewards(&who); + + PendingRewardsOfStakingInnovation::::mutate_exists(&who, |maybe_pending_multi_rewards| { + if let Some(pending_multi_rewards) = maybe_pending_multi_rewards { + for (currency_id, pending_reward) in pending_multi_rewards.iter_mut() { + if pending_reward.is_zero() { + continue; + } + + let payout_amount = pending_reward.clone(); + + match Self::distribute_reward(&who, *currency_id, payout_amount) { + Ok(_) => { + // update state + *pending_reward = Zero::zero(); + + Self::deposit_event(Event::ClaimRewards( + who.clone(), + FungibleTokenId::NativeToken(0), + payout_amount, + )); + } + Err(e) => { + log::error!( + target: "economy", + "staking_payout_reward: failed to payout {:?} to {:?} to {:?}", + pending_reward, who, e + ); + } + } + } + } + }); Ok(()) } @@ -918,10 +1017,44 @@ pub mod pallet { Ok(().into()) } - } - #[pallet::hooks] - impl Hooks for Pallet {} + /// This function only for governance origin to execute when starting the protocol or + /// changes of era duration. + #[pallet::weight(< T as Config >::WeightInfo::stake_b())] + pub fn update_era_config( + origin: OriginFor, + last_era_updated_block: Option>, + frequency: Option>, + estimated_reward_rate_per_era: Option>, + ) -> DispatchResult { + let _ = ensure_root(origin)?; + + if let Some(change) = frequency { + UpdateEraFrequency::::put(change); + Self::deposit_event(Event::::UpdatedInnovationStakingEraFrequency(change)); + } + + if let Some(change) = last_era_updated_block { + let update_era_frequency = UpdateEraFrequency::::get(); + let current_block = >::block_number(); + if !update_era_frequency.is_zero() { + ensure!( + change > current_block.saturating_sub(update_era_frequency) && change <= current_block, + Error::::InvalidLastEraUpdatedBlock + ); + + LastEraUpdatedBlock::::put(change); + Self::deposit_event(Event::::LastInnovationStakingEraUpdated(change)); + } + } + + if let Some(reward_rate_per_era) = estimated_reward_rate_per_era { + EstimatedStakingRewardPerEra::::put(reward_rate_per_era); + Self::deposit_event(Event::::EstimatedRewardPerEraUpdated(reward_rate_per_era)); + } + Ok(()) + } + } } impl Pallet { @@ -948,8 +1081,6 @@ impl Pallet { return Ok(()); } - T::FungibleTokenCurrency::withdraw(T::MiningCurrencyId::get(), who, amount); - Self::deposit_event(Event::::MiningResourceBurned(amount)); Ok(()) @@ -992,4 +1123,288 @@ impl Pallet { current_block_number >= target } + + pub fn add_share(who: &T::AccountId, add_amount: BalanceOf) { + if add_amount.is_zero() { + return; + } + + StakingRewardPoolInfo::::mutate(|pool_info| { + let initial_total_shares = pool_info.total_shares; + pool_info.total_shares = pool_info.total_shares.saturating_add(add_amount); + + let mut withdrawn_inflation = Vec::<(FungibleTokenId, BalanceOf)>::new(); + + pool_info + .rewards + .iter_mut() + .for_each(|(reward_currency, (total_reward, total_withdrawn_reward))| { + let reward_inflation = if initial_total_shares.is_zero() { + Zero::zero() + } else { + U256::from(add_amount.to_owned().saturated_into::()) + .saturating_mul(total_reward.to_owned().saturated_into::().into()) + .checked_div(initial_total_shares.to_owned().saturated_into::().into()) + .unwrap_or_default() + .as_u128() + .saturated_into() + }; + *total_reward = total_reward.saturating_add(reward_inflation); + *total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_inflation); + + withdrawn_inflation.push((*reward_currency, reward_inflation)); + }); + + SharesAndWithdrawnRewards::::mutate(who, |(share, withdrawn_rewards)| { + *share = share.saturating_add(add_amount); + // update withdrawn inflation for each reward currency + withdrawn_inflation + .into_iter() + .for_each(|(reward_currency, reward_inflation)| { + withdrawn_rewards + .entry(reward_currency) + .and_modify(|withdrawn_reward| { + *withdrawn_reward = withdrawn_reward.saturating_add(reward_inflation); + }) + .or_insert(reward_inflation); + }); + }); + }); + } + + pub fn remove_share(who: &T::AccountId, remove_amount: BalanceOf) { + if remove_amount.is_zero() { + return; + } + + // claim rewards firstly + Self::claim_rewards(who); + + SharesAndWithdrawnRewards::::mutate_exists(who, |share_info| { + if let Some((mut share, mut withdrawn_rewards)) = share_info.take() { + let remove_amount = remove_amount.min(share); + + if remove_amount.is_zero() { + return; + } + + StakingRewardPoolInfo::::mutate_exists(|maybe_pool_info| { + if let Some(mut pool_info) = maybe_pool_info.take() { + let removing_share = U256::from(remove_amount.saturated_into::()); + + pool_info.total_shares = pool_info.total_shares.saturating_sub(remove_amount); + + // update withdrawn rewards for each reward currency + withdrawn_rewards + .iter_mut() + .for_each(|(reward_currency, withdrawn_reward)| { + let withdrawn_reward_to_remove: BalanceOf = removing_share + .saturating_mul(withdrawn_reward.to_owned().saturated_into::().into()) + .checked_div(share.saturated_into::().into()) + .unwrap_or_default() + .as_u128() + .saturated_into(); + + if let Some((total_reward, total_withdrawn_reward)) = + pool_info.rewards.get_mut(reward_currency) + { + *total_reward = total_reward.saturating_sub(withdrawn_reward_to_remove); + *total_withdrawn_reward = + total_withdrawn_reward.saturating_sub(withdrawn_reward_to_remove); + + // remove if all reward is withdrawn + if total_reward.is_zero() { + pool_info.rewards.remove(reward_currency); + } + } + *withdrawn_reward = withdrawn_reward.saturating_sub(withdrawn_reward_to_remove); + }); + + if !pool_info.total_shares.is_zero() { + *maybe_pool_info = Some(pool_info); + } + } + }); + + share = share.saturating_sub(remove_amount); + if !share.is_zero() { + *share_info = Some((share, withdrawn_rewards)); + } + } + }); + } + + pub fn claim_rewards(who: &T::AccountId) { + SharesAndWithdrawnRewards::::mutate_exists(who, |maybe_share_withdrawn| { + if let Some((share, withdrawn_rewards)) = maybe_share_withdrawn { + if share.is_zero() { + return; + } + + StakingRewardPoolInfo::::mutate_exists(|maybe_pool_info| { + if let Some(pool_info) = maybe_pool_info { + let total_shares = U256::from(pool_info.total_shares.to_owned().saturated_into::()); + pool_info.rewards.iter_mut().for_each( + |(reward_currency, (total_reward, total_withdrawn_reward))| { + Self::claim_one( + withdrawn_rewards, + *reward_currency, + share.to_owned(), + total_reward.to_owned(), + total_shares, + total_withdrawn_reward, + who, + ); + }, + ); + } + }); + } + }); + } + + #[allow(clippy::too_many_arguments)] // just we need to have all these to do the stuff + fn claim_one( + withdrawn_rewards: &mut BTreeMap>, + reward_currency: FungibleTokenId, + share: BalanceOf, + total_reward: BalanceOf, + total_shares: U256, + total_withdrawn_reward: &mut BalanceOf, + who: &T::AccountId, + ) { + let withdrawn_reward = withdrawn_rewards.get(&reward_currency).copied().unwrap_or_default(); + let reward_to_withdraw = Self::reward_to_withdraw( + share, + total_reward, + total_shares, + withdrawn_reward, + total_withdrawn_reward.to_owned(), + ); + if !reward_to_withdraw.is_zero() { + *total_withdrawn_reward = total_withdrawn_reward.saturating_add(reward_to_withdraw); + withdrawn_rewards.insert(reward_currency, withdrawn_reward.saturating_add(reward_to_withdraw)); + + // pay reward to `who` + Self::reward_payout(who, reward_currency, reward_to_withdraw); + } + } + + fn reward_to_withdraw( + share: BalanceOf, + total_reward: BalanceOf, + total_shares: U256, + withdrawn_reward: BalanceOf, + total_withdrawn_reward: BalanceOf, + ) -> BalanceOf { + let total_reward_proportion: BalanceOf = U256::from(share.saturated_into::()) + .saturating_mul(U256::from(total_reward.saturated_into::())) + .checked_div(total_shares) + .unwrap_or_default() + .as_u128() + .unique_saturated_into(); + total_reward_proportion + .saturating_sub(withdrawn_reward) + .min(total_reward.saturating_sub(total_withdrawn_reward)) + } + + fn reward_payout(who: &T::AccountId, currency_id: FungibleTokenId, payout_amount: BalanceOf) { + if payout_amount.is_zero() { + return; + } + PendingRewardsOfStakingInnovation::::mutate(who, |rewards| { + rewards + .entry(currency_id) + .and_modify(|current| *current = current.saturating_add(payout_amount)) + .or_insert(payout_amount); + }); + } + + /// Ensure atomic + #[transactional] + fn distribute_reward( + who: &T::AccountId, + reward_currency_id: FungibleTokenId, + payout_amount: BalanceOf, + ) -> DispatchResult { + T::FungibleTokenCurrency::transfer( + reward_currency_id, + &Self::get_reward_payout_account_id(), + who, + payout_amount, + )?; + Ok(()) + } + + pub fn get_reward_payout_account_id() -> T::AccountId { + T::RewardPayoutAccount::get().into_account_truncating() + } + + pub fn get_era_index(block_number: BlockNumberFor) -> EraIndex { + block_number + .checked_sub(&Self::last_era_updated_block()) + .and_then(|n| n.checked_div(&Self::update_era_frequency())) + .and_then(|n| TryInto::::try_into(n).ok()) + .unwrap_or_else(Zero::zero) + } + + #[transactional] + pub fn update_current_era(era_index: EraIndex) -> DispatchResult { + let previous_era = Self::current_era(); + let new_era = previous_era.saturating_add(era_index); + + Self::handle_reward_distribution_to_reward_pool_every_era(previous_era, new_era.clone())?; + CurrentEra::::put(new_era.clone()); + LastEraUpdatedBlock::::put(>::block_number()); + + Self::deposit_event(Event::::CurrentInnovationStakingEraUpdated(new_era.clone())); + Ok(()) + } + + fn handle_reward_distribution_to_reward_pool_every_era( + previous_era: EraIndex, + new_era: EraIndex, + ) -> DispatchResult { + let era_changes = new_era.saturating_sub(previous_era); + ensure!(!era_changes.is_zero(), Error::::Unexpected); + // Get reward per era that set up Governance + let reward_per_era = EstimatedStakingRewardPerEra::::get(); + // Get reward holding account + let reward_holding_origin = T::RewardPayoutAccount::get().into_account_truncating(); + let reward_holding_balance = T::Currency::free_balance(&reward_holding_origin); + + if reward_holding_balance.is_zero() { + // Ignore if reward distributor balance is zero + return Ok(()); + } + + let total_reward = reward_per_era.saturating_mul(era_changes.into()); + let mut amount_to_send = total_reward.clone(); + // Make sure user distributor account has enough balance + if amount_to_send > reward_holding_balance { + amount_to_send = reward_holding_balance + } + + Self::accumulate_reward(FungibleTokenId::NativeToken(0), amount_to_send)?; + Ok(()) + } + + pub fn accumulate_reward(reward_currency: FungibleTokenId, reward_increment: BalanceOf) -> DispatchResult { + if reward_increment.is_zero() { + return Ok(()); + } + StakingRewardPoolInfo::::mutate_exists(|maybe_pool_info| -> DispatchResult { + let pool_info = maybe_pool_info.as_mut().ok_or(Error::::RewardPoolDoesNotExist)?; + + pool_info + .rewards + .entry(reward_currency) + .and_modify(|(total_reward, _)| { + *total_reward = total_reward.saturating_add(reward_increment); + }) + .or_insert((reward_increment, Zero::zero())); + + Ok(()) + }) + } } diff --git a/pallets/economy/src/mock.rs b/pallets/economy/src/mock.rs index 015eed8f..9db5fb9d 100644 --- a/pallets/economy/src/mock.rs +++ b/pallets/economy/src/mock.rs @@ -29,7 +29,6 @@ type AccountPublic = ::Signer; pub const ALICE: AccountId = AccountId32::new([1; 32]); pub const BOB: AccountId = AccountId32::new([2; 32]); pub const FREEDY: AccountId = AccountId32::new([3; 32]); - pub const DISTRIBUTOR_COLLECTION_ID: u64 = 0; pub const DISTRIBUTOR_CLASS_ID: ClassId = 0; pub const DISTRIBUTOR_NFT_ASSET_ID: (ClassId, TokenId) = (0, 0); @@ -203,6 +202,7 @@ impl MetaverseStakingTrait for MetaverseStakingHandler { parameter_types! { pub const TreasuryStakingReward: Perbill = Perbill::from_percent(1); pub StorageDepositFee: Balance = 1; + pub const InnovationStakingRewardPayoutAccountPalletId: PalletId = PalletId(*b"bit/rest"); } impl pallet_mining::Config for Runtime { @@ -237,6 +237,7 @@ impl Config for Runtime { type MinimumStake = MinimumStake; type MaximumEstateStake = MaximumEstateStake; type PowerAmountPerBlock = PowerAmountPerBlock; + type RewardPayoutAccount = InnovationStakingRewardPayoutAccountPalletId; type WeightInfo = (); } diff --git a/pallets/economy/src/tests.rs b/pallets/economy/src/tests.rs index 64e848a4..21410ef5 100644 --- a/pallets/economy/src/tests.rs +++ b/pallets/economy/src/tests.rs @@ -67,28 +67,6 @@ fn get_mining_currency() -> FungibleTokenId { ::MiningCurrencyId::get() } -#[test] -fn set_bit_power_exchange_rate_should_fail_bad_origin() { - ExtBuilder::default().build().execute_with(|| { - assert_noop!( - EconomyModule::set_bit_power_exchange_rate(RuntimeOrigin::signed(account(2)), EXCHANGE_RATE), - BadOrigin - ); - }); -} - -#[test] -fn set_bit_power_exchange_rate_should_work() { - ExtBuilder::default().build().execute_with(|| { - assert_ok!(EconomyModule::set_bit_power_exchange_rate( - RuntimeOrigin::root(), - EXCHANGE_RATE - )); - - assert_eq!(EconomyModule::get_bit_power_exchange_rate(), EXCHANGE_RATE); - }); -} - #[test] fn stake_should_fail_insufficient_balance() { ExtBuilder::default().build().execute_with(|| { @@ -541,3 +519,91 @@ fn unstake_new_estate_owner_should_work() { assert_eq!(EconomyModule::total_estate_stake(), 0u128); }); } + +#[test] +fn stake_on_innovation_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(EconomyModule::stake_on_innovation( + RuntimeOrigin::signed(account(1)), + STAKE_BALANCE, + )); + + assert_eq!( + last_event(), + RuntimeEvent::Economy(crate::Event::StakedInnovation(account(1), STAKE_BALANCE)) + ); + + /// Account share of pool reward should be == STAKE_BALANCE + let acc_1_shared_rewards = EconomyModule::shares_and_withdrawn_rewards(account(1)); + assert_eq!(acc_1_shared_rewards, (STAKE_BALANCE, Default::default())); + + /// Do another staking and ensure all working correctly + assert_ok!(EconomyModule::stake_on_innovation( + RuntimeOrigin::signed(account(1)), + 2000, + )); + + assert_eq!( + last_event(), + RuntimeEvent::Economy(crate::Event::StakedInnovation(account(1), 2000)) + ); + + assert_eq!(Balances::reserved_balance(account(1)), STAKE_BALANCE + 2000); + + assert_eq!( + EconomyModule::shares_and_withdrawn_rewards(account(1)), + (3000, Default::default()) + ); + + assert_eq!( + EconomyModule::get_innovation_staking_info(account(1)), + STAKE_BALANCE + 2000 + ); + + assert_eq!(EconomyModule::total_innovation_staking(), STAKE_BALANCE + 2000); + }); +} + +#[test] +fn unstake_on_innovation_should_work() { + ExtBuilder::default().build().execute_with(|| { + assert_ok!(EconomyModule::stake_on_innovation( + RuntimeOrigin::signed(account(1)), + STAKE_BALANCE, + )); + + /// Account share of pool reward should be == STAKE_BALANCE + let acc_1_shared_rewards = EconomyModule::shares_and_withdrawn_rewards(account(1)); + assert_eq!(acc_1_shared_rewards, (STAKE_BALANCE, Default::default())); + + assert_ok!(EconomyModule::unstake_on_innovation( + RuntimeOrigin::signed(account(1)), + UNSTAKE_AMOUNT, + )); + + assert_eq!( + last_event(), + RuntimeEvent::Economy(crate::Event::UnstakedInnovation(account(1), UNSTAKE_AMOUNT)) + ); + + let total_staked_balance = STAKE_BALANCE - UNSTAKE_AMOUNT; + + assert_eq!( + EconomyModule::get_innovation_staking_info(account(1)), + total_staked_balance + ); + assert_eq!(EconomyModule::total_innovation_staking(), total_staked_balance); + let next_round: RoundIndex = CURRENT_ROUND.saturating_add(28u32); + assert_eq!( + EconomyModule::innovation_staking_exit_queue(account(1), next_round), + Some(UNSTAKE_AMOUNT) + ); + + // Make sure unstaked-share are removed + /// Account share of pool reward should be == STAKE_BALANCE + assert_eq!( + EconomyModule::shares_and_withdrawn_rewards(account(1)), + (total_staked_balance, Default::default()) + ); + }); +} diff --git a/runtime/continuum/src/lib.rs b/runtime/continuum/src/lib.rs index 9305ef52..314eca1e 100644 --- a/runtime/continuum/src/lib.rs +++ b/runtime/continuum/src/lib.rs @@ -1605,6 +1605,7 @@ impl crowdloan::Config for Runtime { parameter_types! { pub const MiningCurrencyId: FungibleTokenId = FungibleTokenId::MiningResource(0); pub const PowerAmountPerBlock: u32 = 100; + pub const InnovationStakingRewardPayoutAccountPalletId: PalletId = PalletId(*b"bit/sred"); } impl economy::Config for Runtime { @@ -1620,6 +1621,7 @@ impl economy::Config for Runtime { type PowerAmountPerBlock = PowerAmountPerBlock; type WeightInfo = weights::module_economy::WeightInfo; type MaximumEstateStake = MaximumEstateStake; + type RewardPayoutAccount = InnovationStakingRewardPayoutAccountPalletId; } impl emergency::Config for Runtime { diff --git a/runtime/metaverse/src/lib.rs b/runtime/metaverse/src/lib.rs index 1c8c1381..0f726457 100644 --- a/runtime/metaverse/src/lib.rs +++ b/runtime/metaverse/src/lib.rs @@ -189,7 +189,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { // `spec_version`, and `authoring_version` are the same between Wasm and native. // This value is set to 100 to notify Polkadot-JS App (https://polkadot.js.org/apps) to use // the compatible custom types. - spec_version: 101, + spec_version: 106, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -376,6 +376,7 @@ parameter_types! { pub const PoolAccountPalletId: PalletId = PalletId(*b"bit/pool"); pub const RewardPayoutAccountPalletId: PalletId = PalletId(*b"bit/pout"); pub const RewardHoldingAccountPalletId: PalletId = PalletId(*b"bit/hold"); + pub const InnovationStakingRewardPayoutAccountPalletId: PalletId = PalletId(*b"bit/sred"); pub const MaxAuthorities: u32 = 50; pub const MaxSetIdSessionEntries: u64 = u64::MAX; @@ -1116,6 +1117,7 @@ impl economy::Config for Runtime { type PowerAmountPerBlock = PowerAmountPerBlock; type WeightInfo = weights::module_economy::WeightInfo; type MaximumEstateStake = MaximumEstateStake; + type RewardPayoutAccount = InnovationStakingRewardPayoutAccountPalletId; } impl emergency::Config for Runtime { diff --git a/runtime/pioneer/src/lib.rs b/runtime/pioneer/src/lib.rs index 98dbb679..e13448bc 100644 --- a/runtime/pioneer/src/lib.rs +++ b/runtime/pioneer/src/lib.rs @@ -1612,6 +1612,7 @@ impl crowdloan::Config for Runtime { parameter_types! { pub const MiningCurrencyId: FungibleTokenId = FungibleTokenId::MiningResource(0); pub const PowerAmountPerBlock: u32 = 100; + pub const InnovationStakingRewardPayoutAccountPalletId: PalletId = PalletId(*b"bit/sred"); } impl economy::Config for Runtime { @@ -1627,6 +1628,7 @@ impl economy::Config for Runtime { type PowerAmountPerBlock = PowerAmountPerBlock; type WeightInfo = weights::module_economy::WeightInfo; type MaximumEstateStake = MaximumEstateStake; + type RewardPayoutAccount = InnovationStakingRewardPayoutAccountPalletId; } impl emergency::Config for Runtime {