difftreelog
resolve_unstake
in: master
5 files changed
Cargo.lockdiffbeforeafterboth--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6814,7 +6814,7 @@
[[package]]
name = "pallet-app-promotion"
-version = "0.2.1"
+version = "0.2.2"
dependencies = [
"frame-benchmarking",
"frame-support",
js-packages/scripts/correctStateAfterMaintenance.tsdiffbeforeafterboth--- a/js-packages/scripts/correctStateAfterMaintenance.ts
+++ b/js-packages/scripts/correctStateAfterMaintenance.ts
@@ -37,7 +37,7 @@
const signer = await privateKey(options.donorSeed);
const txs = skippedBlocks.map((b) =>
- api.tx.sudo.sudo(api.tx.appPromotion.forceUnstake(b)));
+ api.tx.appPromotion.resolveSkippedBlocks(b));
const promises = txs.map((tx) => () => helper.signTransaction(signer, tx));
pallets/app-promotion/CHANGELOG.mddiffbeforeafterboth--- a/pallets/app-promotion/CHANGELOG.md
+++ b/pallets/app-promotion/CHANGELOG.md
@@ -4,6 +4,12 @@
<!-- bureaucrate goes here -->
+## [0.2.2] - 2024-03-21
+
+### Changed
+
+- Unstake of skipped blocks is now available to `Signed` origin.
+
## [0.2.1] - 2023-06-23
### Changed
pallets/app-promotion/Cargo.tomldiffbeforeafterboth--- a/pallets/app-promotion/Cargo.toml
+++ b/pallets/app-promotion/Cargo.toml
@@ -9,7 +9,7 @@
license = 'GPLv3'
name = 'pallet-app-promotion'
repository = 'https://github.com/UniqueNetwork/unique-chain'
-version = '0.2.1'
+version = '0.2.2'
[package.metadata.docs.rs]
targets = ['x86_64-unknown-linux-gnu']
pallets/app-promotion/src/lib.rsdiffbeforeafterboth1// Copyright 2019-2022 Unique Network (Gibraltar) Ltd.2// This file is part of Unique Network.34// Unique Network is free software: you can redistribute it and/or modify5// it under the terms of the GNU General Public License as published by6// the Free Software Foundation, either version 3 of the License, or7// (at your option) any later version.89// Unique Network is distributed in the hope that it will be useful,10// but WITHOUT ANY WARRANTY; without even the implied warranty of11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the12// GNU General Public License for more details.1314// You should have received a copy of the GNU General Public License15// along with Unique Network. If not, see <http://www.gnu.org/licenses/>.1617//! # App Promotion pallet18//!19//! The pallet implements the mechanics of staking and sponsoring collections/contracts.20//!21//! - [`Config`]22//! - [`Pallet`]23//! - [`Error`]24//! - [`Event`]25//!26//! ## Overview27//! The App Promotion pallet allows fund holders to stake at a certain daily rate of return.28//! The mechanics implemented in the pallet allow it to act as a sponsor for collections / contracts,29//! the list of which is set by the pallet administrator.30//!31//!32//! ## Interface33//! The pallet provides interfaces for funds, collection/contract operations (see [types] module).3435//!36//! ### Dispatchable Functions37//! - [`set_admin_address`][`Pallet::set_admin_address`] - sets an address as the the admin.38//! - [`stake`][`Pallet::stake`] - stakes the amount of native tokens.39//! - [`unstake`][`Pallet::unstake`] - unstakes all stakes.40//! - [`sponsor_collection`][`Pallet::sponsor_collection`] - sets the pallet to be the sponsor for the collection.41//! - [`stop_sponsoring_collection`][`Pallet::stop_sponsoring_collection`] - removes the pallet as the sponsor for the collection.42//! - [`sponsor_contract`][`Pallet::sponsor_contract`] - sets the pallet to be the sponsor for the contract.43//! - [`stop_sponsoring_contract`][`Pallet::stop_sponsoring_contract`] - removes the pallet as the sponsor for the contract.44//! - [`payout_stakers`][`Pallet::payout_stakers`] - recalculates interest for the specified number of stakers.45//!4647// #![recursion_limit = "1024"]48#![cfg_attr(not(feature = "std"), no_std)]4950#[cfg(feature = "runtime-benchmarks")]51mod benchmarking;5253pub mod types;54pub mod weights;5556use frame_support::{57 dispatch::DispatchResult,58 ensure,59 pallet_prelude::*,60 storage::Key,61 traits::{62 fungible::{Inspect, InspectFreeze, Mutate, MutateFreeze},63 tokens::Balance,64 Get,65 },66 weights::Weight,67 Blake2_128Concat, BoundedVec, PalletId, Twox64Concat,68};69use frame_system::pallet_prelude::*;70pub use pallet::*;71use pallet_evm::account::CrossAccountId;72use parity_scale_codec::EncodeLike;73use sp_core::H160;74use sp_runtime::{75 traits::{AccountIdConversion, BlockNumberProvider, CheckedAdd, CheckedSub, Zero},76 ArithmeticError, DispatchError, Perbill,77};78use sp_std::{borrow::ToOwned, cell::RefCell, iter::Sum, vec, vec::Vec};79pub use types::*;80use up_data_structs::CollectionId;81use weights::WeightInfo;8283const PENDING_LIMIT_PER_BLOCK: u32 = 3;8485type BalanceOf<T> =86 <<T as Config>::Currency as Inspect<<T as frame_system::Config>::AccountId>>::Balance;8788#[frame_support::pallet]89pub mod pallet {9091 use super::*;9293 #[pallet::config]94 pub trait Config:95 frame_system::Config + pallet_evm::Config + pallet_configuration::Config96 {97 /// Type to interact with the native token98 type Currency: MutateFreeze<Self::AccountId> + Mutate<Self::AccountId>;99100 /// Type for interacting with collections101 type CollectionHandler: CollectionHandler<102 AccountId = Self::AccountId,103 CollectionId = CollectionId,104 >;105106 /// Type for interacting with conrtacts107 type ContractHandler: ContractHandler<AccountId = Self::CrossAccountId, ContractId = H160>;108109 /// `AccountId` for treasury110 type TreasuryAccountId: Get<Self::AccountId>;111112 /// The app's pallet id, used for deriving its sovereign account address.113 #[pallet::constant]114 type PalletId: Get<PalletId>;115116 /// Freeze identifier used by the pallet117 #[pallet::constant]118 type FreezeIdentifier: Get<119 <<Self as Config>::Currency as InspectFreeze<Self::AccountId>>::Id,120 >;121122 /// In relay blocks.123 #[pallet::constant]124 type RecalculationInterval: Get<BlockNumberFor<Self>>;125126 /// In parachain blocks.127 #[pallet::constant]128 type PendingInterval: Get<BlockNumberFor<Self>>;129130 /// Rate of return for interval in blocks defined in `RecalculationInterval`.131 #[pallet::constant]132 type IntervalIncome: Get<Perbill>;133134 /// Decimals for the `Currency`.135 #[pallet::constant]136 type Nominal: Get<BalanceOf<Self>>;137138 /// Maintenance mode status.139 type IsMaintenanceModeEnabled: Get<bool>;140141 /// Weight information for extrinsics in this pallet.142 type WeightInfo: WeightInfo;143144 // The relay block number provider145 type RelayBlockNumberProvider: BlockNumberProvider<BlockNumber = BlockNumberFor<Self>>;146147 /// Events compatible with [`frame_system::Config::Event`].148 type RuntimeEvent: IsType<<Self as frame_system::Config>::RuntimeEvent> + From<Event<Self>>;149 }150151 #[pallet::pallet]152 pub struct Pallet<T>(_);153154 #[pallet::event]155 #[pallet::generate_deposit(pub(super) fn deposit_event)]156 pub enum Event<T: Config> {157 /// Staking recalculation was performed158 ///159 /// # Arguments160 /// * AccountId: account of the staker.161 /// * Balance : recalculation base162 /// * Balance : total income163 StakingRecalculation(164 /// An recalculated staker165 T::AccountId,166 /// Base on which interest is calculated167 BalanceOf<T>,168 /// Amount of accrued interest169 BalanceOf<T>,170 ),171172 /// Staking was performed173 ///174 /// # Arguments175 /// * AccountId: account of the staker176 /// * Balance : staking amount177 Stake(T::AccountId, BalanceOf<T>),178179 /// Unstaking was performed180 ///181 /// # Arguments182 /// * AccountId: account of the staker183 /// * Balance : unstaking amount184 Unstake(T::AccountId, BalanceOf<T>),185186 /// The admin was set187 ///188 /// # Arguments189 /// * AccountId: account address of the admin190 SetAdmin(T::AccountId),191 }192193 #[pallet::error]194 pub enum Error<T> {195 /// Error due to action requiring admin to be set.196 AdminNotSet,197 /// No permission to perform an action.198 NoPermission,199 /// Insufficient funds to perform an action.200 NotSufficientFunds,201 /// Occurs when a pending unstake cannot be added in this block. PENDING_LIMIT_PER_BLOCK` limits exceeded.202 PendingForBlockOverflow,203 /// The error is due to the fact that the collection/contract must already be sponsored in order to perform the action.204 SponsorNotSet,205 /// Errors caused by insufficient staked balance.206 InsufficientStakedBalance,207 /// Errors caused by incorrect state of a staker in context of the pallet.208 InconsistencyState,209 }210211 /// Stores the total staked amount.212 #[pallet::storage]213 pub type TotalStaked<T: Config> = StorageValue<Value = BalanceOf<T>, QueryKind = ValueQuery>;214215 /// Stores the `admin` account. Some extrinsics can only be executed if they were signed by `admin`.216 #[pallet::storage]217 pub type Admin<T: Config> = StorageValue<Value = T::AccountId, QueryKind = OptionQuery>;218219 /// Stores the amount of tokens staked by account in the blocknumber.220 ///221 /// * **Key1** - Staker account.222 /// * **Key2** - Relay block number when the stake was made.223 /// * **(Balance, BlockNumber)** - Balance of the stake.224 /// The number of the relay block in which we must perform the interest recalculation225 #[pallet::storage]226 pub type Staked<T: Config> = StorageNMap<227 Key = (228 Key<Blake2_128Concat, T::AccountId>,229 Key<Twox64Concat, BlockNumberFor<T>>,230 ),231 Value = (BalanceOf<T>, BlockNumberFor<T>),232 QueryKind = ValueQuery,233 >;234235 /// Stores number of stake records for an `Account`.236 ///237 /// * **Key** - Staker account.238 /// * **Value** - Amount of stakes.239 #[pallet::storage]240 pub type StakesPerAccount<T: Config> =241 StorageMap<_, Blake2_128Concat, T::AccountId, u8, ValueQuery>;242243 /// Pending unstake records for an `Account`.244 ///245 /// * **Key** - Staker account.246 /// * **Value** - Amount of stakes.247 #[pallet::storage]248 pub type PendingUnstake<T: Config> = StorageMap<249 _,250 Twox64Concat,251 BlockNumberFor<T>,252 BoundedVec<(T::AccountId, BalanceOf<T>), ConstU32<PENDING_LIMIT_PER_BLOCK>>,253 ValueQuery,254 >;255256 /// Stores a key for record for which the revenue recalculation was performed.257 /// If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.258 #[pallet::storage]259 #[pallet::getter(fn get_next_calculated_record)]260 pub type PreviousCalculatedRecord<T: Config> =261 StorageValue<Value = (T::AccountId, BlockNumberFor<T>), QueryKind = OptionQuery>;262263 #[pallet::hooks]264 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {265 /// Block overflow is impossible due to the fact that the unstake algorithm in on_initialize266 /// implies the execution of a strictly limited number of relatively lightweight operations.267 /// A separate benchmark has been implemented to scale the weight depending on the number of pendings.268 fn on_initialize(current_block_number: BlockNumberFor<T>) -> Weight269 where270 BlockNumberFor<T>: From<u32>,271 {272 if T::IsMaintenanceModeEnabled::get() {273 return T::DbWeight::get().reads_writes(1, 0);274 }275276 let block_pending = PendingUnstake::<T>::take(current_block_number);277 let counter = block_pending.len() as u32;278279 if !block_pending.is_empty() {280 block_pending.into_iter().for_each(|(staker, amount)| {281 if let Some(b) = Self::get_frozen_balance(&staker) {282 let new_state = b.checked_sub(&amount).unwrap_or_default();283284 // In this case, setting a new state for the frozen funds cannot fail285 // because the state change goes in the direction of decreasing the frozen funds286 // and the validity of this transition is ensured by the fact287 // that we cannot (in the current implementation) unfreeze more funds288 // than were originally frozen by the pallet. Either way, `on_initialize()` cannot fail.289 Self::set_freeze_unchecked(&staker, new_state);290 };291 });292 }293294 <T as Config>::WeightInfo::on_initialize(counter)295 }296 }297298 #[pallet::call]299 impl<T: Config> Pallet<T>300 where301 BlockNumberFor<T>: From<u32> + Into<u32>,302 <<T as Config>::Currency as Inspect<T::AccountId>>::Balance: Sum + From<u128>,303 {304 /// Sets an address as the the admin.305 ///306 /// # Permissions307 ///308 /// * Sudo309 ///310 /// # Arguments311 ///312 /// * `admin`: account of the new admin.313 #[pallet::call_index(0)]314 #[pallet::weight(<T as Config>::WeightInfo::set_admin_address())]315 pub fn set_admin_address(origin: OriginFor<T>, admin: T::CrossAccountId) -> DispatchResult {316 ensure_root(origin)?;317318 <Admin<T>>::set(Some(admin.as_sub().to_owned()));319320 Self::deposit_event(Event::SetAdmin(admin.as_sub().to_owned()));321322 Ok(())323 }324325 /// Stakes the amount of native tokens.326 /// Sets `amount` to the locked state.327 /// The maximum number of stakes for a staker is 10.328 ///329 /// # Arguments330 ///331 /// * `amount`: in native tokens.332 #[pallet::call_index(1)]333 #[pallet::weight(<T as Config>::WeightInfo::stake())]334 pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {335 let staker_id = ensure_signed(staker)?;336337 ensure!(338 StakesPerAccount::<T>::get(&staker_id) < 10,339 Error::<T>::NoPermission340 );341342 ensure!(343 amount >= <BalanceOf<T>>::from(100u128) * T::Nominal::get(),344 ArithmeticError::Underflow345 );346 let config = <PalletConfiguration<T>>::get();347348 let balance = <<T as Config>::Currency as Inspect<T::AccountId>>::balance(&staker_id);349350 // checks that we can freeze `amount` on the `staker` account.351 ensure!(352 amount353 <= match Self::get_frozen_balance(&staker_id) {354 Some(frozen_by_pallet) => balance355 .checked_sub(&frozen_by_pallet)356 .ok_or(ArithmeticError::Underflow)?,357 None => balance,358 },359 ArithmeticError::Underflow360 );361362 Self::add_freeze_balance(&staker_id, amount)?;363364 let block_number = T::RelayBlockNumberProvider::current_block_number();365366 // Calculation of the number of recalculation periods,367 // after how much the first interest calculation should be performed for the stake368 let recalculate_after_interval: BlockNumberFor<T> =369 if block_number % config.recalculation_interval == 0u32.into() {370 1u32.into()371 } else {372 2u32.into()373 };374375 // Сalculation of the number of the relay block376 // in which it is necessary to accrue remuneration for the stake.377 let recalc_block = (block_number / config.recalculation_interval378 + recalculate_after_interval)379 * config.recalculation_interval;380381 <Staked<T>>::insert((&staker_id, block_number), {382 let mut balance_and_recalc_block = <Staked<T>>::get((&staker_id, block_number));383 balance_and_recalc_block.0 = balance_and_recalc_block384 .0385 .checked_add(&amount)386 .ok_or(ArithmeticError::Overflow)?;387 balance_and_recalc_block.1 = recalc_block;388 balance_and_recalc_block389 });390391 <TotalStaked<T>>::set(392 <TotalStaked<T>>::get()393 .checked_add(&amount)394 .ok_or(ArithmeticError::Overflow)?,395 );396397 StakesPerAccount::<T>::mutate(&staker_id, |stakes| *stakes += 1);398399 Self::deposit_event(Event::Stake(staker_id, amount));400401 Ok(())402 }403404 /// Unstakes all stakes.405 /// After the end of `PendingInterval` this sum becomes completely406 /// free for further use.407 #[pallet::call_index(2)]408 #[pallet::weight(<T as Config>::WeightInfo::unstake_all())]409 pub fn unstake_all(staker: OriginFor<T>) -> DispatchResult {410 let staker_id = ensure_signed(staker)?;411412 Self::unstake_all_internal(staker_id)413 }414415 /// Unstakes the amount of balance for the staker.416 /// After the end of `PendingInterval` this sum becomes completely417 /// free for further use.418 ///419 /// # Arguments420 ///421 /// * `staker`: staker account.422 /// * `amount`: amount of unstaked funds.423 #[pallet::call_index(8)]424 #[pallet::weight(<T as Config>::WeightInfo::unstake_partial())]425 pub fn unstake_partial(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {426 let staker_id = ensure_signed(staker)?;427428 Self::unstake_partial_internal(staker_id, amount)429 }430431 /// Sets the pallet to be the sponsor for the collection.432 ///433 /// # Permissions434 ///435 /// * Pallet admin436 ///437 /// # Arguments438 ///439 /// * `collection_id`: ID of the collection that will be sponsored by `pallet_id`440 #[pallet::call_index(3)]441 #[pallet::weight(<T as Config>::WeightInfo::sponsor_collection())]442 pub fn sponsor_collection(443 admin: OriginFor<T>,444 collection_id: CollectionId,445 ) -> DispatchResult {446 let admin_id = ensure_signed(admin)?;447 ensure!(448 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,449 Error::<T>::NoPermission450 );451452 T::CollectionHandler::set_sponsor(Self::account_id(), collection_id)453 }454455 /// Removes the pallet as the sponsor for the collection.456 /// Returns [`NoPermission`][`Error::NoPermission`]457 /// if the pallet wasn't the sponsor.458 ///459 /// # Permissions460 ///461 /// * Pallet admin462 ///463 /// # Arguments464 ///465 /// * `collection_id`: ID of the collection that is sponsored by `pallet_id`466 #[pallet::call_index(4)]467 #[pallet::weight(<T as Config>::WeightInfo::stop_sponsoring_collection())]468 pub fn stop_sponsoring_collection(469 admin: OriginFor<T>,470 collection_id: CollectionId,471 ) -> DispatchResult {472 let admin_id = ensure_signed(admin)?;473474 ensure!(475 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,476 Error::<T>::NoPermission477 );478479 ensure!(480 T::CollectionHandler::sponsor(collection_id)?.ok_or(<Error<T>>::SponsorNotSet)?481 == Self::account_id(),482 <Error<T>>::NoPermission483 );484 T::CollectionHandler::remove_collection_sponsor(collection_id)485 }486487 /// Sets the pallet to be the sponsor for the contract.488 ///489 /// # Permissions490 ///491 /// * Pallet admin492 ///493 /// # Arguments494 ///495 /// * `contract_id`: the contract address that will be sponsored by `pallet_id`496 #[pallet::call_index(5)]497 #[pallet::weight(<T as Config>::WeightInfo::sponsor_contract())]498 pub fn sponsor_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {499 let admin_id = ensure_signed(admin)?;500501 ensure!(502 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,503 Error::<T>::NoPermission504 );505506 T::ContractHandler::set_sponsor(507 T::CrossAccountId::from_sub(Self::account_id()),508 contract_id,509 )510 }511512 /// Removes the pallet as the sponsor for the contract.513 /// Returns [`NoPermission`][`Error::NoPermission`]514 /// if the pallet wasn't the sponsor.515 ///516 /// # Permissions517 ///518 /// * Pallet admin519 ///520 /// # Arguments521 ///522 /// * `contract_id`: the contract address that is sponsored by `pallet_id`523 #[pallet::call_index(6)]524 #[pallet::weight(<T as Config>::WeightInfo::stop_sponsoring_contract())]525 pub fn stop_sponsoring_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {526 let admin_id = ensure_signed(admin)?;527528 ensure!(529 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,530 Error::<T>::NoPermission531 );532533 ensure!(534 T::ContractHandler::sponsor(contract_id)?535 .ok_or(<Error<T>>::SponsorNotSet)?536 .as_sub() == &Self::account_id(),537 <Error<T>>::NoPermission538 );539 T::ContractHandler::remove_contract_sponsor(contract_id)540 }541542 /// Recalculates interest for the specified number of stakers.543 /// If all stakers are not recalculated, the next call of the extrinsic544 /// will continue the recalculation, from those stakers for whom this545 /// was not perform in last call.546 ///547 /// # Permissions548 ///549 /// * Pallet admin550 ///551 /// # Arguments552 ///553 /// * `stakers_number`: the number of stakers for which recalculation will be performed554 #[pallet::call_index(7)]555 #[pallet::weight(<T as Config>::WeightInfo::payout_stakers(stakers_number.unwrap_or(DEFAULT_NUMBER_PAYOUTS) as u32))]556 pub fn payout_stakers(admin: OriginFor<T>, stakers_number: Option<u8>) -> DispatchResult {557 let admin_id = ensure_signed(admin)?;558559 ensure!(560 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,561 Error::<T>::NoPermission562 );563 let config = <PalletConfiguration<T>>::get();564565 let mut stakers_number = stakers_number.unwrap_or(DEFAULT_NUMBER_PAYOUTS);566567 ensure!(568 stakers_number <= config.max_stakers_per_calculation && stakers_number != 0,569 Error::<T>::NoPermission570 );571572 // calculate the number of the current recalculation block,573 // this is necessary in order to understand which stakers we should calculate interest574 let current_recalc_block = Self::get_current_recalc_block(575 T::RelayBlockNumberProvider::current_block_number(),576 &config,577 );578579 // calculate the number of the next recalculation block,580 // this value is set for the stakers to whom the recalculation will be performed581 let next_recalc_block = current_recalc_block + config.recalculation_interval;582583 let storage_iterator =584 Self::get_next_calculated_key().map_or(Staked::<T>::iter(), Staked::<T>::iter_from);585586 PreviousCalculatedRecord::<T>::set(None);587588 {589 // Address handled in the last payout loop iteration (below)590 let last_id = RefCell::new(None);591 // Block number (as a part of the key) for which calculation was performed in the last payout loop iteration592 let mut last_staked_calculated_block = Default::default();593 // Reward balance for the address in the iteration594 let income_acc = RefCell::new(BalanceOf::<T>::default());595 // Staked balance for the address in the iteration (before stake is recalculated)596 let amount_acc = RefCell::new(BalanceOf::<T>::default());597598 // This closure is used to finalize handling single staker address in each of the two conditions: (1) when we break out of the payout599 // loop because we reached the number of stakes for rewarding, (2) When all stakes by the single address are handled and the payout600 // loop switches to handling the next staker address:601 // 1. Transfer full reward amount to the payee602 // 2. Lock the reward in staking lock603 // 3. Update TotalStaked amount604 // 4. Issue StakingRecalculation event605 let flush_stake = || -> DispatchResult {606 if let Some(last_id) = &*last_id.borrow() {607 if !income_acc.borrow().is_zero() {608 // TO-DO: When moving to ED>0, reconsider the value of preservation609 <<T as Config>::Currency as Mutate<T::AccountId>>::transfer(610 &T::TreasuryAccountId::get(),611 last_id,612 *income_acc.borrow(),613 frame_support::traits::tokens::Preservation::Protect,614 )?;615616 Self::add_freeze_balance(last_id, *income_acc.borrow())?;617 <TotalStaked<T>>::try_mutate(|staked| -> DispatchResult {618 *staked = staked619 .checked_add(&*income_acc.borrow())620 .ok_or(ArithmeticError::Overflow)?;621 Ok(())622 })?;623624 Self::deposit_event(Event::StakingRecalculation(625 last_id.clone(),626 *amount_acc.borrow(),627 *income_acc.borrow(),628 ));629 }630631 *income_acc.borrow_mut() = BalanceOf::<T>::default();632 *amount_acc.borrow_mut() = BalanceOf::<T>::default();633 }634 Ok(())635 };636637 // Reward payment loop. Should loop for no more than config.max_stakers_per_calculation638 // iterations in one extrinsic call639 //640 // stakers_number - keeps the remaining number of iterations (staker addresses to handle)641 // next_recalc_block_for_stake - is taken from the state and stores the starting relay block from which reward should be paid out642 // income_acc - stores the reward amount to pay to the staker address (accumulates over all address stake records)643 for ((current_id, staked_block), (amount, next_recalc_block_for_stake)) in644 storage_iterator645 {646 // last_id is not equal current_id when we switch to handling a new staker address647 // or just start handling the very first address. In the latter case last_id will be None and648 // flush_stake will do nothing649 if last_id.borrow().as_ref() != Some(¤t_id) {650 if stakers_number > 0 {651 flush_stake()?;652 *last_id.borrow_mut() = Some(current_id.clone());653 stakers_number -= 1;654 }655 // Break out if we reached the address limit656 else {657 if let Some(staker) = &*last_id.borrow() {658 // Save the last calculated record to pick up in the next extrinsic call659 PreviousCalculatedRecord::<T>::set(Some((660 staker.clone(),661 last_staked_calculated_block,662 )));663 }664 break;665 };666 };667668 // Increase accumulated reward for current address and update current staking record, i.e. (address, staked_block) -> amount669 if current_recalc_block >= next_recalc_block_for_stake {670 *amount_acc.borrow_mut() += amount;671 Self::recalculate_and_insert_stake(672 ¤t_id,673 staked_block,674 next_recalc_block,675 amount,676 ((current_recalc_block - next_recalc_block_for_stake)677 / config.recalculation_interval)678 .into() + 1,679 &mut *income_acc.borrow_mut(),680 );681 }682 last_staked_calculated_block = staked_block;683 }684 flush_stake()?;685 }686687 Ok(())688 }689690 /// Called for blocks that, for some reason, have not been unstacked691 ///692 ///693 /// # Arguments694 ///695 /// * `origin`: Must be `Signed`.696 /// * `pending_blocks`: Block numbers that will be processed.697 #[pallet::call_index(9)]698 #[pallet::weight(<T as Config>::WeightInfo::on_initialize(PENDING_LIMIT_PER_BLOCK*pending_blocks.len() as u32))]699 pub fn resolve_skipped_blocks(700 origin: OriginFor<T>,701 pending_blocks: Vec<BlockNumberFor<T>>,702 ) -> DispatchResult {703 ensure_signed(origin)?;704705 ensure!(706 pending_blocks707 .iter()708 .all(|b| *b < <frame_system::Pallet<T>>::block_number()),709 <Error<T>>::NoPermission710 );711712 let mut pendings =713 Vec::with_capacity(PENDING_LIMIT_PER_BLOCK as usize * pending_blocks.len());714 pending_blocks715 .into_iter()716 .for_each(|b| pendings.append(&mut PendingUnstake::<T>::take(b).into_inner()));717718 pendings719 .into_iter()720 .try_for_each(|(staker, amount)| -> Result<(), DispatchError> {721 if let Some(b) = Self::get_frozen_balance(&staker) {722 let new_state = b.checked_sub(&amount).unwrap_or_default();723 Self::set_freeze_with_result(&staker, new_state)?;724 }725726 Ok(())727 })?;728729 Ok(())730 }731 }732}733734impl<T: Config> Pallet<T> {735 /// The account address of the app promotion pot.736 ///737 /// This actually does computation. If you need to keep using it, then make sure you cache the738 /// value and only call this once.739 pub fn account_id() -> T::AccountId {740 T::PalletId::get().into_account_truncating()741 }742743 /// Unstakes the balance for the staker.744 ///745 /// - `staker`: staker account.746 /// - `amount`: amount of unstaked funds.747 fn unstake_partial_internal(748 staker_id: T::AccountId,749 unstaked_balance: BalanceOf<T>,750 ) -> DispatchResult {751 if unstaked_balance == Default::default() {752 return Ok(());753 }754755 let config = <PalletConfiguration<T>>::get();756757 // calculate block number where the sum would be free758 let unpending_block = <frame_system::Pallet<T>>::block_number() + config.pending_interval;759760 let mut pendings = <PendingUnstake<T>>::get(unpending_block);761762 // checks that we can do unstake in the block763 ensure!(!pendings.is_full(), Error::<T>::PendingForBlockOverflow);764765 let mut stakes = Staked::<T>::iter_prefix((&staker_id,)).collect::<Vec<_>>();766767 let total_staked = stakes768 .iter()769 .fold(<BalanceOf<T>>::default(), |acc, (_, (balance, _))| {770 acc + *balance771 });772773 ensure!(774 unstaked_balance <= total_staked,775 <Error<T>>::InsufficientStakedBalance776 );777778 <TotalStaked<T>>::set(779 <TotalStaked<T>>::get()780 .checked_sub(&unstaked_balance)781 .ok_or(ArithmeticError::Underflow)?,782 );783784 stakes.sort_by_key(|(block, _)| *block);785786 let mut acc_amount = unstaked_balance;787 let mut will_deleted_stakes_count = 0u8;788789 let changed_stakes = stakes790 .into_iter()791 .map_while(|(block, (balance_per_block, _))| {792 if acc_amount == <BalanceOf<T>>::default() {793 return None;794 }795 if acc_amount < balance_per_block {796 let res = (block, balance_per_block - acc_amount);797 acc_amount = <BalanceOf<T>>::default();798 Some(res)799 } else {800 acc_amount -= balance_per_block;801 will_deleted_stakes_count += 1;802 Some((block, <BalanceOf<T>>::default()))803 }804 })805 .collect::<Vec<_>>();806807 pendings808 .try_push((staker_id.clone(), unstaked_balance))809 .map_err(|_| Error::<T>::PendingForBlockOverflow)?;810811 StakesPerAccount::<T>::try_mutate(&staker_id, |stakes| -> DispatchResult {812 *stakes = stakes813 .checked_sub(will_deleted_stakes_count)814 .ok_or(ArithmeticError::Underflow)?;815 Ok(())816 })?;817818 changed_stakes819 .into_iter()820 .for_each(|(staked_block, current_stake_state)| {821 if current_stake_state == Default::default() {822 <Staked<T>>::remove((&staker_id, staked_block));823 } else {824 <Staked<T>>::mutate((&staker_id, staked_block), |(old_stake_state, _)| {825 *old_stake_state = current_stake_state826 });827 }828 });829830 <PendingUnstake<T>>::insert(unpending_block, pendings);831832 Self::deposit_event(Event::Unstake(staker_id, unstaked_balance));833834 Ok(())835 }836837 /// Adds the balance to frozen by the pallet.838 ///839 /// - `staker`: staker account.840 /// - `amount`: amount of added frozen funds.841 fn add_freeze_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {842 Self::get_frozen_balance(staker)843 .unwrap_or_default()844 .checked_add(&amount)845 .map(|freeze| Self::set_freeze_with_result(staker, freeze))846 .ok_or::<DispatchError>(ArithmeticError::Overflow.into())?847 }848849 /// Sets the new state of a balance frozen by the pallet.850 ///851 /// - `staker`: staker account.852 /// - `amount`: amount of frozen funds.853 fn set_freeze_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {854 let _ = Self::set_freeze_with_result(staker, amount);855 }856857 /// Sets the new state of a balance frozen by the pallet.858 ///859 /// - `staker`: staker account.860 /// - `amount`: amount of frozen funds.861 fn set_freeze_with_result(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {862 if amount.is_zero() {863 <<T as Config>::Currency as MutateFreeze<T::AccountId>>::thaw(864 &T::FreezeIdentifier::get(),865 staker,866 )867 } else {868 <<T as Config>::Currency as MutateFreeze<T::AccountId>>::set_freeze(869 &T::FreezeIdentifier::get(),870 staker,871 amount,872 )873 }874 }875876 /// Returns the balance frozen by the pallet for the staker.877 ///878 /// - `staker`: staker account.879 pub fn get_frozen_balance(staker: &T::AccountId) -> Option<BalanceOf<T>> {880 let res = <<T as Config>::Currency as InspectFreeze<T::AccountId>>::balance_frozen(881 &T::FreezeIdentifier::get(),882 staker,883 );884885 if res == Zero::zero() {886 None887 } else {888 Some(res)889 }890 }891892 /// Returns the total staked balance for the staker.893 ///894 /// - `staker`: staker account.895 pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {896 let staked = Staked::<T>::iter_prefix((staker,))897 .fold(<BalanceOf<T>>::default(), |acc, (_, (amount, _))| {898 acc + amount899 });900 if staked != <BalanceOf<T>>::default() {901 Some(staked)902 } else {903 None904 }905 }906907 /// Returns all relay block numbers when stake was made,908 /// the amount of the stake.909 ///910 /// - `staker`: staker account.911 pub fn total_staked_by_id_per_block(912 staker: impl EncodeLike<T::AccountId>,913 ) -> Option<Vec<(BlockNumberFor<T>, BalanceOf<T>)>> {914 let mut staked = Staked::<T>::iter_prefix((staker,))915 .map(|(block, (amount, _))| (block, amount))916 .collect::<Vec<_>>();917 staked.sort_by_key(|(block, _)| *block);918 if !staked.is_empty() {919 Some(staked)920 } else {921 None922 }923 }924925 /// Returns the total staked balance for the staker.926 /// If `staker` is `None`, returns the total amount staked.927 /// - `staker`: staker account.928 pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {929 staker.map_or(Some(<TotalStaked<T>>::get()), |s| {930 Self::total_staked_by_id(s.as_sub())931 })932 }933934 /// Returns all relay block numbers when stake was made,935 /// the amount of the stake.936 ///937 /// - `staker`: staker account.938 pub fn cross_id_total_staked_per_block(939 staker: T::CrossAccountId,940 ) -> Vec<(BlockNumberFor<T>, BalanceOf<T>)> {941 Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()942 }943944 fn recalculate_and_insert_stake(945 staker: &T::AccountId,946 staked_block: BlockNumberFor<T>,947 next_recalc_block: BlockNumberFor<T>,948 base: BalanceOf<T>,949 iters: u32,950 income_acc: &mut BalanceOf<T>,951 ) {952 let income = Self::calculate_income(base, iters);953954 if let Some(res) = base.checked_add(&income) {955 <Staked<T>>::insert((staker, staked_block), (res, next_recalc_block));956 *income_acc += income;957 };958 }959960 fn calculate_income<I>(base: I, iters: u32) -> I961 where962 I: EncodeLike<BalanceOf<T>> + Balance,963 {964 let config = <PalletConfiguration<T>>::get();965 let mut income = base;966967 (0..iters).for_each(|_| income += config.interval_income * income);968969 income - base970 }971972 /// Get relay block number rounded down to multiples of config.recalculation_interval.973 /// We need it to reward stakers in integer parts of recalculation_interval974 fn get_current_recalc_block(975 current_relay_block: BlockNumberFor<T>,976 config: &PalletConfiguration<T>,977 ) -> BlockNumberFor<T> {978 (current_relay_block / config.recalculation_interval) * config.recalculation_interval979 }980981 fn get_next_calculated_key() -> Option<Vec<u8>> {982 Self::get_next_calculated_record().map(Staked::<T>::hashed_key_for)983 }984}985986impl<T: Config> Pallet<T>987where988 <<T as Config>::Currency as Inspect<T::AccountId>>::Balance: Sum,989{990 /// Returns the amount reserved by the pending.991 /// If `staker` is `None`, returns the total pending.992 ///993 /// -`staker`: staker account.994 ///995 /// Since user funds are not transferred anywhere by staking, overflow protection is provided996 /// at the level of the associated type `Balance` of `Currency` trait. In order to overflow,997 /// the staker must have more funds on his account than the maximum set for `Balance` type.998 pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {999 staker.map_or(1000 PendingUnstake::<T>::iter_values()1001 .flat_map(|pendings| pendings.into_iter().map(|(_, amount)| amount))1002 .sum(),1003 |s| {1004 PendingUnstake::<T>::iter_values()1005 .flatten()1006 .filter_map(|(id, amount)| {1007 if id == *s.as_sub() {1008 Some(amount)1009 } else {1010 None1011 }1012 })1013 .sum()1014 },1015 )1016 }10171018 /// Returns all parachain block numbers when unreserve is expected,1019 /// the amount of the unreserved funds.1020 ///1021 /// - `staker`: staker account.1022 pub fn cross_id_pending_unstake_per_block(1023 staker: T::CrossAccountId,1024 ) -> Vec<(BlockNumberFor<T>, BalanceOf<T>)> {1025 let mut unsorted_res = vec![];1026 PendingUnstake::<T>::iter().for_each(|(block, pendings)| {1027 pendings.into_iter().for_each(|(id, amount)| {1028 if id == *staker.as_sub() {1029 unsorted_res.push((block, amount));1030 };1031 })1032 });10331034 unsorted_res.sort_by_key(|(block, _)| *block);1035 unsorted_res1036 }10371038 fn unstake_all_internal(staker_id: T::AccountId) -> DispatchResult {1039 let config = <PalletConfiguration<T>>::get();10401041 // calculate block number where the sum would be free1042 let block = <frame_system::Pallet<T>>::block_number() + config.pending_interval;10431044 let mut pendings = <PendingUnstake<T>>::get(block);10451046 // checks that we can do unstake in the block1047 ensure!(!pendings.is_full(), Error::<T>::PendingForBlockOverflow);10481049 let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))1050 .map(|(_, (amount, _))| amount)1051 .sum();10521053 if total_staked.is_zero() {1054 return Ok(());1055 }10561057 pendings1058 .try_push((staker_id.clone(), total_staked))1059 .map_err(|_| Error::<T>::PendingForBlockOverflow)?;10601061 <PendingUnstake<T>>::insert(block, pendings);10621063 TotalStaked::<T>::set(1064 TotalStaked::<T>::get()1065 .checked_sub(&total_staked)1066 .ok_or(ArithmeticError::Underflow)?,1067 );10681069 StakesPerAccount::<T>::remove(&staker_id);10701071 Self::deposit_event(Event::Unstake(staker_id, total_staked));10721073 Ok(())1074 }1075}