difftreelog
fix unstake
in: master
2 files changed
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 promotion18//!19//! The app promotion pallet is designed to ... .20//!21//! ## Interface22//!23//! ### Dispatchable Functions24//!25//! * `start_inflation` - This method sets the inflation start date. Can be only called once.26//! Inflation start block can be backdated and will catch up. The method will create Treasury27//! account if it does not exist and perform the first inflation deposit.2829// #![recursion_limit = "1024"]30#![cfg_attr(not(feature = "std"), no_std)]3132#[cfg(feature = "runtime-benchmarks")]33mod benchmarking;34#[cfg(test)]35mod tests;36pub mod types;37pub mod weights;3839use sp_std::{40 vec::{Vec},41 vec,42 iter::Sum,43 borrow::ToOwned,44};45use sp_core::H160;46use codec::EncodeLike;47use pallet_balances::BalanceLock;48pub use types::*;4950// use up_common::constants::{DAYS, UNIQUE};51use up_data_structs::CollectionId;5253use frame_support::{54 dispatch::{DispatchResult},55 traits::{56 Currency, Get, LockableCurrency, WithdrawReasons, tokens::Balance, ExistenceRequirement,57 },58 ensure,59};6061use weights::WeightInfo;6263pub use pallet::*;64use pallet_evm::account::CrossAccountId;65use sp_runtime::{66 Perbill,67 traits::{BlockNumberProvider, CheckedAdd, CheckedSub, AccountIdConversion, Zero},68 ArithmeticError,69};7071pub const LOCK_IDENTIFIER: [u8; 8] = *b"appstake";72const PENDING_LIMIT_PER_BLOCK: u32 = 3;7374type BalanceOf<T> =75 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;7677// const SECONDS_TO_BLOCK: u32 = 6;78// const DAY: u32 = 60 * 60 * 24 / SECONDS_TO_BLOCK;79// const WEEK: u32 = 7 * DAY;80// const TWO_WEEK: u32 = 2 * WEEK;81// const YEAR: u32 = DAY * 365;8283#[frame_support::pallet]84pub mod pallet {85 use super::*;86 use frame_support::{87 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId,88 traits::ReservableCurrency,89 };90 use frame_system::pallet_prelude::*;9192 #[pallet::config]93 pub trait Config: frame_system::Config + pallet_evm::account::Config {94 type Currency: ExtendedLockableCurrency<Self::AccountId>95 + ReservableCurrency<Self::AccountId>;9697 type CollectionHandler: CollectionHandler<98 AccountId = Self::AccountId,99 CollectionId = CollectionId,100 >;101102 type ContractHandler: ContractHandler<AccountId = Self::CrossAccountId, ContractId = H160>;103104 type TreasuryAccountId: Get<Self::AccountId>;105106 /// The app's pallet id, used for deriving its sovereign account ID.107 #[pallet::constant]108 type PalletId: Get<PalletId>;109110 /// In relay blocks.111 #[pallet::constant]112 type RecalculationInterval: Get<Self::BlockNumber>;113 /// In relay blocks.114 #[pallet::constant]115 type PendingInterval: Get<Self::BlockNumber>;116117 /// In chain blocks.118 #[pallet::constant]119 type Day: Get<Self::BlockNumber>; // useless120121 #[pallet::constant]122 type Nominal: Get<BalanceOf<Self>>;123124 #[pallet::constant]125 type IntervalIncome: Get<Perbill>;126127 /// Weight information for extrinsics in this pallet.128 type WeightInfo: WeightInfo;129130 // The relay block number provider131 type RelayBlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;132133 /// Events compatible with [`frame_system::Config::Event`].134 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;135 }136137 #[pallet::pallet]138 #[pallet::generate_store(pub(super) trait Store)]139 pub struct Pallet<T>(_);140141 #[pallet::event]142 #[pallet::generate_deposit(fn deposit_event)]143 pub enum Event<T: Config> {144 StakingRecalculation(145 /// An recalculated staker146 T::AccountId,147 /// Base on which interest is calculated148 BalanceOf<T>,149 /// Amount of accrued interest150 BalanceOf<T>,151 ),152 }153154 #[pallet::error]155 pub enum Error<T> {156 /// Error due to action requiring admin to be set157 AdminNotSet,158 /// No permission to perform an action159 NoPermission,160 /// Insufficient funds to perform an action161 NotSufficientFounds,162 PendingForBlockOverflow,163 /// An error related to the fact that an invalid argument was passed to perform an action164 InvalidArgument,165 }166167 #[pallet::storage]168 pub type TotalStaked<T: Config> = StorageValue<Value = BalanceOf<T>, QueryKind = ValueQuery>;169170 #[pallet::storage]171 pub type Admin<T: Config> = StorageValue<Value = T::AccountId, QueryKind = OptionQuery>;172173 /// Amount of tokens staked by account in the blocknumber.174 #[pallet::storage]175 pub type Staked<T: Config> = StorageNMap<176 Key = (177 Key<Blake2_128Concat, T::AccountId>,178 Key<Twox64Concat, T::BlockNumber>,179 ),180 Value = (BalanceOf<T>, T::BlockNumber),181 QueryKind = ValueQuery,182 >;183 /// Amount of stakes for an Account184 #[pallet::storage]185 pub type StakesPerAccount<T: Config> =186 StorageMap<_, Blake2_128Concat, T::AccountId, u8, ValueQuery>;187188 /// Amount of tokens pending unstake per user per block.189 // #[pallet::storage]190 // pub type PendingUnstake<T: Config> = StorageNMap<191 // Key = (192 // Key<Blake2_128Concat, T::AccountId>,193 // Key<Twox64Concat, T::BlockNumber>,194 // ),195 // Value = BalanceOf<T>,196 // QueryKind = ValueQuery,197 // >;198 #[pallet::storage]199 pub type PendingUnstake<T: Config> = StorageMap<200 _,201 Twox64Concat,202 T::BlockNumber,203 BoundedVec<(T::AccountId, BalanceOf<T>), ConstU32<PENDING_LIMIT_PER_BLOCK>>,204 ValueQuery,205 >;206207 /// A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.208 #[pallet::storage]209 pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;210211 // /// Next target block when interest is recalculated212 // #[pallet::storage]213 // #[pallet::getter(fn get_interest_block)]214 // pub type NextInterestBlock<T: Config> =215 // StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;216217 /// Stores hash a record for which the last revenue recalculation was performed.218 /// If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.219 #[pallet::storage]220 #[pallet::getter(fn get_next_calculated_record)]221 pub type NextCalculatedRecord<T: Config> =222 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;223224 #[pallet::hooks]225 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {226 fn on_initialize(current_block_number: T::BlockNumber) -> Weight227 where228 <T as frame_system::Config>::BlockNumber: From<u32>,229 {230 let mut consumed_weight = 0;231 let mut add_weight = |reads, writes, weight| {232 consumed_weight += T::DbWeight::get().reads_writes(reads, writes);233 consumed_weight += weight;234 };235236 let block_pending = PendingUnstake::<T>::take(current_block_number);237238 add_weight(0, 1, 0);239240 if !block_pending.is_empty() {241 block_pending.into_iter().for_each(|(staker, amount)| {242 <T::Currency as ReservableCurrency<T::AccountId>>::unreserve(&staker, amount);243 });244 }245246 consumed_weight247 }248 }249250 #[pallet::call]251 impl<T: Config> Pallet<T>252 where253 T::BlockNumber: From<u32> + Into<u32>,254 <<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum + From<u128>,255 {256 #[pallet::weight(T::WeightInfo::set_admin_address())]257 pub fn set_admin_address(origin: OriginFor<T>, admin: T::CrossAccountId) -> DispatchResult {258 ensure_root(origin)?;259 <Admin<T>>::set(Some(admin.as_sub().to_owned()));260261 Ok(())262 }263264 // #[pallet::weight(T::WeightInfo::start_app_promotion())]265 // pub fn start_app_promotion(266 // origin: OriginFor<T>,267 // promotion_start_relay_block: Option<T::BlockNumber>,268 // ) -> DispatchResult269 // where270 // <T as frame_system::Config>::BlockNumber: From<u32>,271 // {272 // ensure_root(origin)?;273274 // // Start app-promotion mechanics if it has not been yet initialized275 // if <StartBlock<T>>::get() == 0u32.into() {276 // let start_block = promotion_start_relay_block277 // .unwrap_or(T::RelayBlockNumberProvider::current_block_number());278279 // // Set promotion global start block280 // <StartBlock<T>>::set(start_block);281282 // <NextInterestBlock<T>>::set(start_block + T::RecalculationInterval::get());283 // }284285 // Ok(())286 // }287288 // #[pallet::weight(T::WeightInfo::stop_app_promotion())]289 // pub fn stop_app_promotion(origin: OriginFor<T>) -> DispatchResult290 // where291 // <T as frame_system::Config>::BlockNumber: From<u32>,292 // {293 // ensure_root(origin)?;294295 // if <StartBlock<T>>::get() != 0u32.into() {296 // <StartBlock<T>>::set(T::BlockNumber::default());297 // <NextInterestBlock<T>>::set(T::BlockNumber::default());298 // }299300 // Ok(())301 // }302303 #[pallet::weight(T::WeightInfo::stake())]304 pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {305 let staker_id = ensure_signed(staker)?;306307 ensure!(308 StakesPerAccount::<T>::get(&staker_id) < 10,309 Error::<T>::NoPermission310 );311312 ensure!(313 amount >= Into::<BalanceOf<T>>::into(100u128) * T::Nominal::get(),314 ArithmeticError::Underflow315 );316317 let balance =318 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);319320 ensure!(balance >= amount, ArithmeticError::Underflow);321322 <<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(323 &staker_id,324 amount,325 WithdrawReasons::all(),326 balance - amount,327 )?;328329 Self::add_lock_balance(&staker_id, amount)?;330331 let block_number = T::RelayBlockNumberProvider::current_block_number();332 let recalc_block = (block_number / T::RecalculationInterval::get() + 2u32.into())333 * T::RecalculationInterval::get();334335 <Staked<T>>::insert((&staker_id, block_number), {336 let mut balance_and_recalc_block = <Staked<T>>::get((&staker_id, block_number));337 balance_and_recalc_block.0 = balance_and_recalc_block338 .0339 .checked_add(&amount)340 .ok_or(ArithmeticError::Overflow)?;341 balance_and_recalc_block.1 = recalc_block;342 balance_and_recalc_block343 });344345 <TotalStaked<T>>::set(346 <TotalStaked<T>>::get()347 .checked_add(&amount)348 .ok_or(ArithmeticError::Overflow)?,349 );350351 StakesPerAccount::<T>::mutate(&staker_id, |stakes| *stakes += 1);352 Ok(())353 }354355 #[pallet::weight(T::WeightInfo::unstake())]356 pub fn unstake(staker: OriginFor<T>) -> DispatchResultWithPostInfo {357 let staker_id = ensure_signed(staker)?;358 let block = <frame_system::Pallet<T>>::block_number() + T::PendingInterval::get();359 let mut pendings = <PendingUnstake<T>>::get(block);360361 ensure!(pendings.is_full(), Error::<T>::PendingForBlockOverflow);362363 let mut total_stakes = 0u64;364365 let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))366 .map(|(_, (amount, _))| {367 total_stakes += 1;368 amount369 })370 .sum();371372 if total_staked.is_zero() {373 return Ok(None.into());374 }375376 pendings377 .try_push((staker_id.clone(), total_staked))378 .map_err(|_| Error::<T>::PendingForBlockOverflow)?;379380 <PendingUnstake<T>>::insert(block, pendings);381382 Self::unlock_balance_unchecked(&staker_id, total_staked);383384 <T::Currency as ReservableCurrency<T::AccountId>>::reserve(&staker_id, total_staked)?;385386 TotalStaked::<T>::set(387 TotalStaked::<T>::get()388 .checked_sub(&total_staked)389 .ok_or(ArithmeticError::Underflow)?,390 ); // when error we should recover initial stake state for the staker391392 StakesPerAccount::<T>::remove(&staker_id);393394 Ok(None.into())395 }396397 #[pallet::weight(T::WeightInfo::sponsor_collection())]398 pub fn sponsor_collection(399 admin: OriginFor<T>,400 collection_id: CollectionId,401 ) -> DispatchResult {402 let admin_id = ensure_signed(admin)?;403 ensure!(404 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,405 Error::<T>::NoPermission406 );407408 T::CollectionHandler::set_sponsor(Self::account_id(), collection_id)409 }410 #[pallet::weight(T::WeightInfo::stop_sponsoring_collection())]411 pub fn stop_sponsoring_collection(412 admin: OriginFor<T>,413 collection_id: CollectionId,414 ) -> DispatchResult {415 let admin_id = ensure_signed(admin)?;416417 ensure!(418 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,419 Error::<T>::NoPermission420 );421422 ensure!(423 T::CollectionHandler::get_sponsor(collection_id)?424 .ok_or(<Error<T>>::InvalidArgument)?425 == Self::account_id(),426 <Error<T>>::NoPermission427 );428 T::CollectionHandler::remove_collection_sponsor(collection_id)429 }430431 #[pallet::weight(T::WeightInfo::sponsor_contract())]432 pub fn sponsor_conract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {433 let admin_id = ensure_signed(admin)?;434435 ensure!(436 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,437 Error::<T>::NoPermission438 );439440 T::ContractHandler::set_sponsor(441 T::CrossAccountId::from_sub(Self::account_id()),442 contract_id,443 )444 }445446 #[pallet::weight(T::WeightInfo::stop_sponsoring_contract())]447 pub fn stop_sponsoring_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {448 let admin_id = ensure_signed(admin)?;449450 ensure!(451 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,452 Error::<T>::NoPermission453 );454455 ensure!(456 T::ContractHandler::get_sponsor(contract_id)?.ok_or(<Error<T>>::InvalidArgument)?457 == T::CrossAccountId::from_sub(Self::account_id()),458 <Error<T>>::NoPermission459 );460 T::ContractHandler::remove_contract_sponsor(contract_id)461 }462463 #[pallet::weight(0)]464 pub fn payout_stakers(admin: OriginFor<T>, stakers_number: Option<u8>) -> DispatchResult {465 let admin_id = ensure_signed(admin)?;466467 ensure!(468 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,469 Error::<T>::NoPermission470 );471472 let current_recalc_block =473 Self::get_current_recalc_block(T::RelayBlockNumberProvider::current_block_number());474 let next_recalc_block = current_recalc_block + T::RecalculationInterval::get();475476 let mut storage_iterator = Self::get_next_calculated_key()477 .map_or(Staked::<T>::iter().skip(0), |key| {478 Staked::<T>::iter_from(key).skip(1)479 });480481 NextCalculatedRecord::<T>::set(None);482483 {484 let mut stakers_number = stakers_number.unwrap_or(20);485 let mut current_id = admin_id;486 let mut income_acc = BalanceOf::<T>::default();487488 while let Some(((id, staked_block), (amount, next_recalc_block_for_stake))) =489 storage_iterator.next()490 {491 if current_id != id {492 if income_acc != BalanceOf::<T>::default() {493 <T::Currency as Currency<T::AccountId>>::transfer(494 &T::TreasuryAccountId::get(),495 ¤t_id,496 income_acc,497 ExistenceRequirement::KeepAlive,498 )499 .and_then(|_| Self::add_lock_balance(¤t_id, income_acc))?;500501 Self::deposit_event(Event::StakingRecalculation(502 current_id, amount, income_acc,503 ));504 }505506 if stakers_number == 0 {507 NextCalculatedRecord::<T>::set(Some((id, staked_block)));508 break;509 }510 stakers_number -= 1;511 income_acc = BalanceOf::<T>::default();512 current_id = id;513 };514 if current_recalc_block >= next_recalc_block_for_stake {515 Self::recalculate_and_insert_stake(516 ¤t_id,517 staked_block,518 next_recalc_block,519 amount,520 ((current_recalc_block - next_recalc_block_for_stake)521 / T::RecalculationInterval::get())522 .into() + 1,523 &mut income_acc,524 );525 }526 }527 }528529 Ok(())530 }531 }532}533534impl<T: Config> Pallet<T> {535 pub fn account_id() -> T::AccountId {536 T::PalletId::get().into_account_truncating()537 }538539 fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {540 let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();541 locked_balance -= amount;542 Self::set_lock_unchecked(staker, locked_balance);543 }544545 fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {546 Self::get_locked_balance(staker)547 .map_or(<BalanceOf<T>>::default(), |l| l.amount)548 .checked_add(&amount)549 .map(|new_lock| Self::set_lock_unchecked(staker, new_lock))550 .ok_or(ArithmeticError::Overflow.into())551 }552553 fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {554 if amount.is_zero() {555 <T::Currency as LockableCurrency<T::AccountId>>::remove_lock(LOCK_IDENTIFIER, &staker);556 } else {557 <T::Currency as LockableCurrency<T::AccountId>>::set_lock(558 LOCK_IDENTIFIER,559 staker,560 amount,561 WithdrawReasons::all(),562 )563 }564 }565566 pub fn get_locked_balance(567 staker: impl EncodeLike<T::AccountId>,568 ) -> Option<BalanceLock<BalanceOf<T>>> {569 <T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)570 .into_iter()571 .find(|l| l.id == LOCK_IDENTIFIER)572 }573574 pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {575 let staked = Staked::<T>::iter_prefix((staker,))576 .into_iter()577 .fold(<BalanceOf<T>>::default(), |acc, (_, (amount, _))| {578 acc + amount579 });580 if staked != <BalanceOf<T>>::default() {581 Some(staked)582 } else {583 None584 }585 }586587 pub fn total_staked_by_id_per_block(588 staker: impl EncodeLike<T::AccountId>,589 ) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {590 let mut staked = Staked::<T>::iter_prefix((staker,))591 .into_iter()592 .map(|(block, (amount, _))| (block, amount))593 .collect::<Vec<_>>();594 staked.sort_by_key(|(block, _)| *block);595 if !staked.is_empty() {596 Some(staked)597 } else {598 None599 }600 }601602 pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {603 staker.map_or(Some(<TotalStaked<T>>::get()), |s| {604 Self::total_staked_by_id(s.as_sub())605 })606 // Self::total_staked_by_id(staker.as_sub())607 }608609 pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {610 Self::get_locked_balance(staker.as_sub())611 .map(|l| l.amount)612 .unwrap_or_default()613 }614615 pub fn cross_id_total_staked_per_block(616 staker: T::CrossAccountId,617 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {618 Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()619 }620621 fn recalculate_and_insert_stake(622 staker: &T::AccountId,623 staked_block: T::BlockNumber,624 next_recalc_block: T::BlockNumber,625 base: BalanceOf<T>,626 iters: u32,627 income_acc: &mut BalanceOf<T>,628 ) {629 let income = Self::calculate_income(base, iters);630631 base.checked_add(&income).map(|res| {632 <Staked<T>>::insert((staker, staked_block), (res, next_recalc_block));633 *income_acc += income;634 });635 }636637 fn calculate_income<I>(base: I, iters: u32) -> I638 where639 I: EncodeLike<BalanceOf<T>> + Balance,640 {641 let mut income = base;642643 (0..iters).for_each(|_| income += T::IntervalIncome::get() * income);644645 income - base646 }647648 fn get_current_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {649 (current_relay_block / T::RecalculationInterval::get()) * T::RecalculationInterval::get()650 }651652 // fn get_next_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {653 // Self::get_current_recalc_block(current_relay_block) + T::RecalculationInterval::get()654 // }655656 fn get_next_calculated_key() -> Option<Vec<u8>> {657 Self::get_next_calculated_record().map(|key| Staked::<T>::hashed_key_for(key))658 }659}660661impl<T: Config> Pallet<T>662where663 <<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum,664{665 pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {666 staker.map_or(667 PendingUnstake::<T>::iter_values()668 .flat_map(|pendings| pendings.into_iter().map(|(_, amount)| amount))669 .sum(),670 |s| {671 PendingUnstake::<T>::iter_values()672 .flatten()673 .filter_map(|(id, amount)| {674 if id == *s.as_sub() {675 Some(amount)676 } else {677 None678 }679 })680 .sum()681 },682 )683 }684685 pub fn cross_id_pending_unstake_per_block(686 staker: T::CrossAccountId,687 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {688 let mut unsorted_res = vec![];689 PendingUnstake::<T>::iter().for_each(|(block, pendings)| {690 pendings.into_iter().for_each(|(id, amount)| {691 if id == *staker.as_sub() {692 unsorted_res.push((block, amount));693 };694 })695 });696697 unsorted_res.sort_by_key(|(block, _)| *block);698 unsorted_res699 }700}1// 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 promotion18//!19//! The app promotion pallet is designed to ... .20//!21//! ## Interface22//!23//! ### Dispatchable Functions24//!25//! * `start_inflation` - This method sets the inflation start date. Can be only called once.26//! Inflation start block can be backdated and will catch up. The method will create Treasury27//! account if it does not exist and perform the first inflation deposit.2829// #![recursion_limit = "1024"]30#![cfg_attr(not(feature = "std"), no_std)]3132#[cfg(feature = "runtime-benchmarks")]33mod benchmarking;34#[cfg(test)]35mod tests;36pub mod types;37pub mod weights;3839use sp_std::{40 vec::{Vec},41 vec,42 iter::Sum,43 borrow::ToOwned,44};45use sp_core::H160;46use codec::EncodeLike;47use pallet_balances::BalanceLock;48pub use types::*;4950// use up_common::constants::{DAYS, UNIQUE};51use up_data_structs::CollectionId;5253use frame_support::{54 dispatch::{DispatchResult},55 traits::{56 Currency, Get, LockableCurrency, WithdrawReasons, tokens::Balance, ExistenceRequirement,57 },58 ensure,59};6061use weights::WeightInfo;6263pub use pallet::*;64use pallet_evm::account::CrossAccountId;65use sp_runtime::{66 Perbill,67 traits::{BlockNumberProvider, CheckedAdd, CheckedSub, AccountIdConversion, Zero},68 ArithmeticError,69};7071pub const LOCK_IDENTIFIER: [u8; 8] = *b"appstake";72const PENDING_LIMIT_PER_BLOCK: u32 = 3;7374type BalanceOf<T> =75 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;7677// const SECONDS_TO_BLOCK: u32 = 6;78// const DAY: u32 = 60 * 60 * 24 / SECONDS_TO_BLOCK;79// const WEEK: u32 = 7 * DAY;80// const TWO_WEEK: u32 = 2 * WEEK;81// const YEAR: u32 = DAY * 365;8283#[frame_support::pallet]84pub mod pallet {85 use super::*;86 use frame_support::{87 Blake2_128Concat, Twox64Concat, pallet_prelude::*, storage::Key, PalletId,88 traits::ReservableCurrency,89 };90 use frame_system::pallet_prelude::*;9192 #[pallet::config]93 pub trait Config: frame_system::Config + pallet_evm::account::Config {94 type Currency: ExtendedLockableCurrency<Self::AccountId>95 + ReservableCurrency<Self::AccountId>;9697 type CollectionHandler: CollectionHandler<98 AccountId = Self::AccountId,99 CollectionId = CollectionId,100 >;101102 type ContractHandler: ContractHandler<AccountId = Self::CrossAccountId, ContractId = H160>;103104 type TreasuryAccountId: Get<Self::AccountId>;105106 /// The app's pallet id, used for deriving its sovereign account ID.107 #[pallet::constant]108 type PalletId: Get<PalletId>;109110 /// In relay blocks.111 #[pallet::constant]112 type RecalculationInterval: Get<Self::BlockNumber>;113 /// In relay blocks.114 #[pallet::constant]115 type PendingInterval: Get<Self::BlockNumber>;116117 /// In chain blocks.118 #[pallet::constant]119 type Day: Get<Self::BlockNumber>; // useless120121 #[pallet::constant]122 type Nominal: Get<BalanceOf<Self>>;123124 #[pallet::constant]125 type IntervalIncome: Get<Perbill>;126127 /// Weight information for extrinsics in this pallet.128 type WeightInfo: WeightInfo;129130 // The relay block number provider131 type RelayBlockNumberProvider: BlockNumberProvider<BlockNumber = Self::BlockNumber>;132133 /// Events compatible with [`frame_system::Config::Event`].134 type Event: IsType<<Self as frame_system::Config>::Event> + From<Event<Self>>;135 }136137 #[pallet::pallet]138 #[pallet::generate_store(pub(super) trait Store)]139 pub struct Pallet<T>(_);140141 #[pallet::event]142 #[pallet::generate_deposit(fn deposit_event)]143 pub enum Event<T: Config> {144 StakingRecalculation(145 /// An recalculated staker146 T::AccountId,147 /// Base on which interest is calculated148 BalanceOf<T>,149 /// Amount of accrued interest150 BalanceOf<T>,151 ),152 }153154 #[pallet::error]155 pub enum Error<T> {156 /// Error due to action requiring admin to be set157 AdminNotSet,158 /// No permission to perform an action159 NoPermission,160 /// Insufficient funds to perform an action161 NotSufficientFounds,162 PendingForBlockOverflow,163 /// An error related to the fact that an invalid argument was passed to perform an action164 InvalidArgument,165 }166167 #[pallet::storage]168 pub type TotalStaked<T: Config> = StorageValue<Value = BalanceOf<T>, QueryKind = ValueQuery>;169170 #[pallet::storage]171 pub type Admin<T: Config> = StorageValue<Value = T::AccountId, QueryKind = OptionQuery>;172173 /// Amount of tokens staked by account in the blocknumber.174 #[pallet::storage]175 pub type Staked<T: Config> = StorageNMap<176 Key = (177 Key<Blake2_128Concat, T::AccountId>,178 Key<Twox64Concat, T::BlockNumber>,179 ),180 Value = (BalanceOf<T>, T::BlockNumber),181 QueryKind = ValueQuery,182 >;183 /// Amount of stakes for an Account184 #[pallet::storage]185 pub type StakesPerAccount<T: Config> =186 StorageMap<_, Blake2_128Concat, T::AccountId, u8, ValueQuery>;187188 /// Amount of tokens pending unstake per user per block.189 // #[pallet::storage]190 // pub type PendingUnstake<T: Config> = StorageNMap<191 // Key = (192 // Key<Blake2_128Concat, T::AccountId>,193 // Key<Twox64Concat, T::BlockNumber>,194 // ),195 // Value = BalanceOf<T>,196 // QueryKind = ValueQuery,197 // >;198 #[pallet::storage]199 pub type PendingUnstake<T: Config> = StorageMap<200 _,201 Twox64Concat,202 T::BlockNumber,203 BoundedVec<(T::AccountId, BalanceOf<T>), ConstU32<PENDING_LIMIT_PER_BLOCK>>,204 ValueQuery,205 >;206207 /// A block when app-promotion has started .I think this is redundant, because we only need `NextInterestBlock`.208 #[pallet::storage]209 pub type StartBlock<T: Config> = StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;210211 // /// Next target block when interest is recalculated212 // #[pallet::storage]213 // #[pallet::getter(fn get_interest_block)]214 // pub type NextInterestBlock<T: Config> =215 // StorageValue<Value = T::BlockNumber, QueryKind = ValueQuery>;216217 /// Stores hash a record for which the last revenue recalculation was performed.218 /// If `None`, then recalculation has not yet been performed or calculations have been completed for all stakers.219 #[pallet::storage]220 #[pallet::getter(fn get_next_calculated_record)]221 pub type NextCalculatedRecord<T: Config> =222 StorageValue<Value = (T::AccountId, T::BlockNumber), QueryKind = OptionQuery>;223224 #[pallet::hooks]225 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {226 fn on_initialize(current_block_number: T::BlockNumber) -> Weight227 where228 <T as frame_system::Config>::BlockNumber: From<u32>,229 {230 let mut consumed_weight = 0;231 let mut add_weight = |reads, writes, weight| {232 consumed_weight += T::DbWeight::get().reads_writes(reads, writes);233 consumed_weight += weight;234 };235236 let block_pending = PendingUnstake::<T>::take(current_block_number);237238 add_weight(0, 1, 0);239240 if !block_pending.is_empty() {241 block_pending.into_iter().for_each(|(staker, amount)| {242 <T::Currency as ReservableCurrency<T::AccountId>>::unreserve(&staker, amount);243 });244 }245246 consumed_weight247 }248 }249250 #[pallet::call]251 impl<T: Config> Pallet<T>252 where253 T::BlockNumber: From<u32> + Into<u32>,254 <<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum + From<u128>,255 {256 #[pallet::weight(T::WeightInfo::set_admin_address())]257 pub fn set_admin_address(origin: OriginFor<T>, admin: T::CrossAccountId) -> DispatchResult {258 ensure_root(origin)?;259 <Admin<T>>::set(Some(admin.as_sub().to_owned()));260261 Ok(())262 }263264 // #[pallet::weight(T::WeightInfo::start_app_promotion())]265 // pub fn start_app_promotion(266 // origin: OriginFor<T>,267 // promotion_start_relay_block: Option<T::BlockNumber>,268 // ) -> DispatchResult269 // where270 // <T as frame_system::Config>::BlockNumber: From<u32>,271 // {272 // ensure_root(origin)?;273274 // // Start app-promotion mechanics if it has not been yet initialized275 // if <StartBlock<T>>::get() == 0u32.into() {276 // let start_block = promotion_start_relay_block277 // .unwrap_or(T::RelayBlockNumberProvider::current_block_number());278279 // // Set promotion global start block280 // <StartBlock<T>>::set(start_block);281282 // <NextInterestBlock<T>>::set(start_block + T::RecalculationInterval::get());283 // }284285 // Ok(())286 // }287288 // #[pallet::weight(T::WeightInfo::stop_app_promotion())]289 // pub fn stop_app_promotion(origin: OriginFor<T>) -> DispatchResult290 // where291 // <T as frame_system::Config>::BlockNumber: From<u32>,292 // {293 // ensure_root(origin)?;294295 // if <StartBlock<T>>::get() != 0u32.into() {296 // <StartBlock<T>>::set(T::BlockNumber::default());297 // <NextInterestBlock<T>>::set(T::BlockNumber::default());298 // }299300 // Ok(())301 // }302303 #[pallet::weight(T::WeightInfo::stake())]304 pub fn stake(staker: OriginFor<T>, amount: BalanceOf<T>) -> DispatchResult {305 let staker_id = ensure_signed(staker)?;306307 ensure!(308 StakesPerAccount::<T>::get(&staker_id) < 10,309 Error::<T>::NoPermission310 );311312 ensure!(313 amount >= Into::<BalanceOf<T>>::into(100u128) * T::Nominal::get(),314 ArithmeticError::Underflow315 );316317 let balance =318 <<T as Config>::Currency as Currency<T::AccountId>>::free_balance(&staker_id);319320 ensure!(balance >= amount, ArithmeticError::Underflow);321322 <<T as Config>::Currency as Currency<T::AccountId>>::ensure_can_withdraw(323 &staker_id,324 amount,325 WithdrawReasons::all(),326 balance - amount,327 )?;328329 Self::add_lock_balance(&staker_id, amount)?;330331 let block_number = T::RelayBlockNumberProvider::current_block_number();332 let recalc_block = (block_number / T::RecalculationInterval::get() + 2u32.into())333 * T::RecalculationInterval::get();334335 <Staked<T>>::insert((&staker_id, block_number), {336 let mut balance_and_recalc_block = <Staked<T>>::get((&staker_id, block_number));337 balance_and_recalc_block.0 = balance_and_recalc_block338 .0339 .checked_add(&amount)340 .ok_or(ArithmeticError::Overflow)?;341 balance_and_recalc_block.1 = recalc_block;342 balance_and_recalc_block343 });344345 <TotalStaked<T>>::set(346 <TotalStaked<T>>::get()347 .checked_add(&amount)348 .ok_or(ArithmeticError::Overflow)?,349 );350351 StakesPerAccount::<T>::mutate(&staker_id, |stakes| *stakes += 1);352 Ok(())353 }354355 #[pallet::weight(T::WeightInfo::unstake())]356 pub fn unstake(staker: OriginFor<T>) -> DispatchResultWithPostInfo {357 let staker_id = ensure_signed(staker)?;358 let block = <frame_system::Pallet<T>>::block_number() + T::PendingInterval::get();359 let mut pendings = <PendingUnstake<T>>::get(block);360361 ensure!(!pendings.is_full(), Error::<T>::PendingForBlockOverflow);362363 let mut total_stakes = 0u64;364365 let total_staked: BalanceOf<T> = Staked::<T>::drain_prefix((&staker_id,))366 .map(|(_, (amount, _))| {367 total_stakes += 1;368 amount369 })370 .sum();371372 if total_staked.is_zero() {373 return Ok(None.into()); // TO-DO374 }375376 pendings377 .try_push((staker_id.clone(), total_staked))378 .map_err(|_| Error::<T>::PendingForBlockOverflow)?;379380 <PendingUnstake<T>>::insert(block, pendings);381382 Self::unlock_balance_unchecked(&staker_id, total_staked);383384 <T::Currency as ReservableCurrency<T::AccountId>>::reserve(&staker_id, total_staked)?;385386 TotalStaked::<T>::set(387 TotalStaked::<T>::get()388 .checked_sub(&total_staked)389 .ok_or(ArithmeticError::Underflow)?,390 );391392 StakesPerAccount::<T>::remove(&staker_id);393394 Ok(None.into())395 }396397 #[pallet::weight(T::WeightInfo::sponsor_collection())]398 pub fn sponsor_collection(399 admin: OriginFor<T>,400 collection_id: CollectionId,401 ) -> DispatchResult {402 let admin_id = ensure_signed(admin)?;403 ensure!(404 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,405 Error::<T>::NoPermission406 );407408 T::CollectionHandler::set_sponsor(Self::account_id(), collection_id)409 }410 #[pallet::weight(T::WeightInfo::stop_sponsoring_collection())]411 pub fn stop_sponsoring_collection(412 admin: OriginFor<T>,413 collection_id: CollectionId,414 ) -> DispatchResult {415 let admin_id = ensure_signed(admin)?;416417 ensure!(418 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,419 Error::<T>::NoPermission420 );421422 ensure!(423 T::CollectionHandler::get_sponsor(collection_id)?424 .ok_or(<Error<T>>::InvalidArgument)?425 == Self::account_id(),426 <Error<T>>::NoPermission427 );428 T::CollectionHandler::remove_collection_sponsor(collection_id)429 }430431 #[pallet::weight(T::WeightInfo::sponsor_contract())]432 pub fn sponsor_conract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {433 let admin_id = ensure_signed(admin)?;434435 ensure!(436 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,437 Error::<T>::NoPermission438 );439440 T::ContractHandler::set_sponsor(441 T::CrossAccountId::from_sub(Self::account_id()),442 contract_id,443 )444 }445446 #[pallet::weight(T::WeightInfo::stop_sponsoring_contract())]447 pub fn stop_sponsoring_contract(admin: OriginFor<T>, contract_id: H160) -> DispatchResult {448 let admin_id = ensure_signed(admin)?;449450 ensure!(451 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,452 Error::<T>::NoPermission453 );454455 ensure!(456 T::ContractHandler::get_sponsor(contract_id)?.ok_or(<Error<T>>::InvalidArgument)?457 == T::CrossAccountId::from_sub(Self::account_id()),458 <Error<T>>::NoPermission459 );460 T::ContractHandler::remove_contract_sponsor(contract_id)461 }462463 #[pallet::weight(0)]464 pub fn payout_stakers(admin: OriginFor<T>, stakers_number: Option<u8>) -> DispatchResult {465 let admin_id = ensure_signed(admin)?;466467 ensure!(468 admin_id == Admin::<T>::get().ok_or(Error::<T>::AdminNotSet)?,469 Error::<T>::NoPermission470 );471472 let current_recalc_block =473 Self::get_current_recalc_block(T::RelayBlockNumberProvider::current_block_number());474 let next_recalc_block = current_recalc_block + T::RecalculationInterval::get();475476 let mut storage_iterator = Self::get_next_calculated_key()477 .map_or(Staked::<T>::iter(), |key| {478 Staked::<T>::iter_from(key)479 });480481 NextCalculatedRecord::<T>::set(None);482483 {484 let mut stakers_number = stakers_number.unwrap_or(20);485 let mut last_id = admin_id;486 let mut income_acc = BalanceOf::<T>::default();487488 while let Some(((current_id, staked_block), (amount, next_recalc_block_for_stake))) =489 storage_iterator.next()490 {491 if last_id != current_id {492 if income_acc != BalanceOf::<T>::default() {493 <T::Currency as Currency<T::AccountId>>::transfer(494 &T::TreasuryAccountId::get(),495 &last_id,496 income_acc,497 ExistenceRequirement::KeepAlive,498 )499 .and_then(|_| Self::add_lock_balance(&last_id, income_acc))?;500501 Self::deposit_event(Event::StakingRecalculation(502 last_id, amount, income_acc,503 ));504 }505506 if stakers_number == 0 {507 NextCalculatedRecord::<T>::set(Some((current_id, staked_block)));508 break;509 }510 stakers_number -= 1;511 income_acc = BalanceOf::<T>::default();512 last_id = current_id;513 };514 if current_recalc_block >= next_recalc_block_for_stake {515 Self::recalculate_and_insert_stake(516 &last_id,517 staked_block,518 next_recalc_block,519 amount,520 ((current_recalc_block - next_recalc_block_for_stake)521 / T::RecalculationInterval::get())522 .into() + 1,523 &mut income_acc,524 );525 }526 }527 }528529 Ok(())530 }531 }532}533534impl<T: Config> Pallet<T> {535 pub fn account_id() -> T::AccountId {536 T::PalletId::get().into_account_truncating()537 }538539 fn unlock_balance_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {540 let mut locked_balance = Self::get_locked_balance(staker).map(|l| l.amount).unwrap();541 locked_balance -= amount;542 Self::set_lock_unchecked(staker, locked_balance);543 }544545 fn add_lock_balance(staker: &T::AccountId, amount: BalanceOf<T>) -> DispatchResult {546 Self::get_locked_balance(staker)547 .map_or(<BalanceOf<T>>::default(), |l| l.amount)548 .checked_add(&amount)549 .map(|new_lock| Self::set_lock_unchecked(staker, new_lock))550 .ok_or(ArithmeticError::Overflow.into())551 }552553 fn set_lock_unchecked(staker: &T::AccountId, amount: BalanceOf<T>) {554 if amount.is_zero() {555 <T::Currency as LockableCurrency<T::AccountId>>::remove_lock(LOCK_IDENTIFIER, &staker);556 } else {557 <T::Currency as LockableCurrency<T::AccountId>>::set_lock(558 LOCK_IDENTIFIER,559 staker,560 amount,561 WithdrawReasons::all(),562 )563 }564 }565566 pub fn get_locked_balance(567 staker: impl EncodeLike<T::AccountId>,568 ) -> Option<BalanceLock<BalanceOf<T>>> {569 <T::Currency as ExtendedLockableCurrency<T::AccountId>>::locks(staker)570 .into_iter()571 .find(|l| l.id == LOCK_IDENTIFIER)572 }573574 pub fn total_staked_by_id(staker: impl EncodeLike<T::AccountId>) -> Option<BalanceOf<T>> {575 let staked = Staked::<T>::iter_prefix((staker,))576 .into_iter()577 .fold(<BalanceOf<T>>::default(), |acc, (_, (amount, _))| {578 acc + amount579 });580 if staked != <BalanceOf<T>>::default() {581 Some(staked)582 } else {583 None584 }585 }586587 pub fn total_staked_by_id_per_block(588 staker: impl EncodeLike<T::AccountId>,589 ) -> Option<Vec<(T::BlockNumber, BalanceOf<T>)>> {590 let mut staked = Staked::<T>::iter_prefix((staker,))591 .into_iter()592 .map(|(block, (amount, _))| (block, amount))593 .collect::<Vec<_>>();594 staked.sort_by_key(|(block, _)| *block);595 if !staked.is_empty() {596 Some(staked)597 } else {598 None599 }600 }601602 pub fn cross_id_total_staked(staker: Option<T::CrossAccountId>) -> Option<BalanceOf<T>> {603 staker.map_or(Some(<TotalStaked<T>>::get()), |s| {604 Self::total_staked_by_id(s.as_sub())605 })606 // Self::total_staked_by_id(staker.as_sub())607 }608609 pub fn cross_id_locked_balance(staker: T::CrossAccountId) -> BalanceOf<T> {610 Self::get_locked_balance(staker.as_sub())611 .map(|l| l.amount)612 .unwrap_or_default()613 }614615 pub fn cross_id_total_staked_per_block(616 staker: T::CrossAccountId,617 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {618 Self::total_staked_by_id_per_block(staker.as_sub()).unwrap_or_default()619 }620621 fn recalculate_and_insert_stake(622 staker: &T::AccountId,623 staked_block: T::BlockNumber,624 next_recalc_block: T::BlockNumber,625 base: BalanceOf<T>,626 iters: u32,627 income_acc: &mut BalanceOf<T>,628 ) {629 let income = Self::calculate_income(base, iters);630631 base.checked_add(&income).map(|res| {632 <Staked<T>>::insert((staker, staked_block), (res, next_recalc_block));633 *income_acc += income;634 });635 }636637 fn calculate_income<I>(base: I, iters: u32) -> I638 where639 I: EncodeLike<BalanceOf<T>> + Balance,640 {641 let mut income = base;642643 (0..iters).for_each(|_| income += T::IntervalIncome::get() * income);644645 income - base646 }647648 fn get_current_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {649 (current_relay_block / T::RecalculationInterval::get()) * T::RecalculationInterval::get()650 }651652 // fn get_next_recalc_block(current_relay_block: T::BlockNumber) -> T::BlockNumber {653 // Self::get_current_recalc_block(current_relay_block) + T::RecalculationInterval::get()654 // }655656 fn get_next_calculated_key() -> Option<Vec<u8>> {657 Self::get_next_calculated_record().map(|key| Staked::<T>::hashed_key_for(key))658 }659}660661impl<T: Config> Pallet<T>662where663 <<T as Config>::Currency as Currency<T::AccountId>>::Balance: Sum,664{665 pub fn cross_id_pending_unstake(staker: Option<T::CrossAccountId>) -> BalanceOf<T> {666 staker.map_or(667 PendingUnstake::<T>::iter_values()668 .flat_map(|pendings| pendings.into_iter().map(|(_, amount)| amount))669 .sum(),670 |s| {671 PendingUnstake::<T>::iter_values()672 .flatten()673 .filter_map(|(id, amount)| {674 if id == *s.as_sub() {675 Some(amount)676 } else {677 None678 }679 })680 .sum()681 },682 )683 }684685 pub fn cross_id_pending_unstake_per_block(686 staker: T::CrossAccountId,687 ) -> Vec<(T::BlockNumber, BalanceOf<T>)> {688 let mut unsorted_res = vec![];689 PendingUnstake::<T>::iter().for_each(|(block, pendings)| {690 pendings.into_iter().for_each(|(id, amount)| {691 if id == *staker.as_sub() {692 unsorted_res.push((block, amount));693 };694 })695 });696697 unsorted_res.sort_by_key(|(block, _)| *block);698 unsorted_res699 }700}tests/src/app-promotion.test.tsdiffbeforeafterboth--- a/tests/src/app-promotion.test.ts
+++ b/tests/src/app-promotion.test.ts
@@ -51,14 +51,12 @@
if (!promotionStartBlock) {
promotionStartBlock = (await helper.api!.query.parachainSystem.lastRelayChainBlockNumber()).toNumber();
}
- await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.startAppPromotion(promotionStartBlock!)));
accounts = await helper.arrange.createCrowd(100, 1000n, alice); // create accounts-pool to speed up tests
});
});
after(async function () {
await usingPlaygrounds(async (helper) => {
- await helper.signTransaction(alice, helper.api!.tx.sudo.sudo(helper.api!.tx.promotion.stopAppPromotion()));
});
});
@@ -241,7 +239,7 @@
it('should be possible for different accounts in one block', async () => {
await usingPlaygrounds(async (helper) => {
- const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!, accounts.pop()!];
+ const stakers = [accounts.pop()!, accounts.pop()!, accounts.pop()!];
await Promise.all(stakers.map(staker => helper.staking.stake(staker, 100n * nominal)));
await Promise.all(stakers.map(staker => helper.staking.unstake(staker)));