pallet_capacity/
types.rs

1//! Types for the Capacity Pallet
2use super::*;
3use common_primitives::capacity::RewardEra;
4use frame_support::{
5	pallet_prelude::PhantomData, BoundedVec, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
6};
7use parity_scale_codec::{Decode, Encode, EncodeLike, MaxEncodedLen};
8use scale_info::TypeInfo;
9use sp_runtime::{
10	traits::{AtLeast32BitUnsigned, CheckedAdd, CheckedSub, Get, Saturating, Zero},
11	BoundedBTreeMap, RuntimeDebug,
12};
13#[cfg(any(feature = "runtime-benchmarks", test))]
14extern crate alloc;
15#[cfg(any(feature = "runtime-benchmarks", test))]
16use alloc::vec::Vec;
17
18/// How much, as a percentage of staked token, to boost a targeted Provider when staking.
19/// this value should be between 0 and 100
20pub const STAKED_PERCENTAGE_TO_BOOST: u32 = 50;
21
22#[derive(
23	Clone, Copy, Debug, Decode, Encode, TypeInfo, Eq, MaxEncodedLen, PartialEq, PartialOrd,
24)]
25/// The type of staking a given Staking Account is doing.
26pub enum StakingType {
27	/// Staking account targets Providers for capacity only, no token reward
28	MaximumCapacity,
29	/// Staking account targets Providers and splits reward between capacity to the Provider
30	/// and token for the account holder
31	ProviderBoost,
32}
33
34/// The type used for storing information about staking details.
35#[derive(
36	TypeInfo, RuntimeDebugNoBound, PartialEqNoBound, EqNoBound, Clone, Decode, Encode, MaxEncodedLen,
37)]
38#[scale_info(skip_type_params(T))]
39pub struct StakingDetails<T: Config> {
40	/// The amount a Staker has staked, minus the sum of all tokens in `unlocking`.
41	pub active: BalanceOf<T>,
42	/// The type of staking for this staking account
43	pub staking_type: StakingType,
44}
45
46/// The type that is used to record a single request for a number of tokens to be unlocked.
47#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
48pub struct UnlockChunk<Balance, EpochNumber> {
49	/// Amount to be unfrozen.
50	pub value: Balance,
51	/// Block number at which point funds are unfrozen.
52	pub thaw_at: EpochNumber,
53}
54
55impl<T: Config> StakingDetails<T> {
56	/// Increases total and active balances by an amount.
57	pub fn deposit(&mut self, amount: BalanceOf<T>) -> Option<()> {
58		self.active = amount.checked_add(&self.active)?;
59		Some(())
60	}
61
62	/// Decrease the amount of active stake by an amount and create an UnlockChunk.
63	pub fn withdraw(&mut self, amount: BalanceOf<T>) -> Result<BalanceOf<T>, DispatchError> {
64		let current_active = self.active;
65
66		let mut new_active = self.active.saturating_sub(amount);
67		let mut actual_unstaked: BalanceOf<T> = amount;
68
69		if new_active.le(&T::MinimumStakingAmount::get()) {
70			actual_unstaked = current_active;
71			new_active = Zero::zero();
72		}
73
74		self.active = new_active;
75		Ok(actual_unstaked)
76	}
77}
78
79impl<T: Config> Default for StakingDetails<T> {
80	fn default() -> Self {
81		Self { active: Zero::zero(), staking_type: StakingType::MaximumCapacity }
82	}
83}
84
85/// Details about the total token amount targeted to an MSA.
86/// The Capacity that the target will receive.
87#[derive(Default, PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
88pub struct StakingTargetDetails<Balance>
89where
90	Balance: Default + Saturating + Copy + CheckedAdd + CheckedSub,
91{
92	/// The total amount of tokens that have been targeted to the MSA.
93	pub amount: Balance,
94	/// The total Capacity that an MSA received.
95	pub capacity: Balance,
96}
97
98impl<Balance: Default + Saturating + Copy + CheckedAdd + CheckedSub + PartialOrd>
99	StakingTargetDetails<Balance>
100{
101	/// Increase an MSA target Staking total and Capacity amount.
102	pub fn deposit(&mut self, amount: Balance, capacity: Balance) -> Option<()> {
103		self.amount = amount.checked_add(&self.amount)?;
104		self.capacity = capacity.checked_add(&self.capacity)?;
105		Some(())
106	}
107
108	/// Decrease an MSA target Staking total and Capacity amount.
109	/// If the amount would put you below the minimum, zero out the amount.
110	/// Return the actual amounts withdrawn.
111	pub fn withdraw(
112		&mut self,
113		amount: Balance,
114		capacity: Balance,
115		minimum: Balance,
116	) -> (Balance, Balance) {
117		let entire_amount = self.amount;
118		let entire_capacity = self.capacity;
119		self.amount = self.amount.saturating_sub(amount);
120		if self.amount.lt(&minimum) {
121			*self = Self::default();
122			return (entire_amount, entire_capacity);
123		} else {
124			self.capacity = self.capacity.saturating_sub(capacity);
125		}
126		(amount, capacity)
127	}
128}
129
130/// The type for storing Registered Provider Capacity balance:
131#[derive(PartialEq, Eq, Clone, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
132pub struct CapacityDetails<Balance, EpochNumber> {
133	/// The Capacity remaining for the `last_replenished_epoch`.
134	pub remaining_capacity: Balance,
135	/// The amount of tokens staked to an MSA.
136	pub total_tokens_staked: Balance,
137	/// The total Capacity issued to an MSA.
138	pub total_capacity_issued: Balance,
139	/// The last Epoch that an MSA was replenished with Capacity.
140	pub last_replenished_epoch: EpochNumber,
141}
142
143impl<Balance, EpochNumber> CapacityDetails<Balance, EpochNumber>
144where
145	Balance: Saturating + Copy + CheckedAdd + CheckedSub,
146	EpochNumber: Clone + PartialOrd + PartialEq,
147{
148	/// Increase a targets total Tokens staked and Capacity total issuance by an amount.
149	/// To be called on a stake
150	pub fn deposit(&mut self, amount: &Balance, capacity: &Balance) -> Option<()> {
151		self.total_tokens_staked = amount.checked_add(&self.total_tokens_staked)?;
152		self.remaining_capacity = capacity.checked_add(&self.remaining_capacity)?;
153		self.total_capacity_issued = capacity.checked_add(&self.total_capacity_issued)?;
154
155		// We do not touch last_replenished epoch here, because it would create a DoS vulnerability.
156		// Since capacity is lazily replenished, an attacker could stake
157		// a minimum amount, then at the very beginning of each epoch, stake a tiny additional amount,
158		// thus preventing replenishment when "last_replenished_at" is checked on the next provider's
159		// message.
160		Some(())
161	}
162
163	/// Return whether capacity can be replenished, given the current epoch.
164	pub fn can_replenish(&self, current_epoch: EpochNumber) -> bool {
165		self.last_replenished_epoch.lt(&current_epoch)
166	}
167
168	/// Completely refill all available capacity.
169	/// To be called lazily when a Capacity message is sent in a new epoch.
170	pub fn replenish_all(&mut self, current_epoch: &EpochNumber) {
171		self.remaining_capacity = self.total_capacity_issued;
172		self.last_replenished_epoch = current_epoch.clone();
173	}
174
175	/// Replenish remaining capacity by the provided amount and
176	/// touch last_replenished_epoch with the current epoch.
177	pub fn replenish_by_amount(&mut self, amount: Balance, current_epoch: &EpochNumber) {
178		self.remaining_capacity = amount.saturating_add(self.remaining_capacity);
179		self.last_replenished_epoch = current_epoch.clone();
180	}
181
182	/// Deduct the given amount from the remaining capacity that can be used to pay for messages.
183	/// To be called when a message is paid for with capacity.
184	pub fn deduct_capacity_by_amount(&mut self, amount: Balance) -> Result<(), ArithmeticError> {
185		let new_remaining =
186			self.remaining_capacity.checked_sub(&amount).ok_or(ArithmeticError::Underflow)?;
187		self.remaining_capacity = new_remaining;
188		Ok(())
189	}
190
191	/// Decrease a target's total available capacity.
192	/// To be called on an unstake.
193	pub fn withdraw(&mut self, capacity_deduction: Balance, tokens_staked_deduction: Balance) {
194		self.total_tokens_staked = self.total_tokens_staked.saturating_sub(tokens_staked_deduction);
195		self.total_capacity_issued = self.total_capacity_issued.saturating_sub(capacity_deduction);
196		self.remaining_capacity = self.remaining_capacity.saturating_sub(capacity_deduction);
197	}
198}
199
200/// The type for storing details about an epoch.
201/// May evolve to store other needed data such as epoch_end.
202#[derive(
203	PartialEq, Eq, Clone, Default, PartialOrd, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
204)]
205pub struct EpochInfo<BlockNumber> {
206	/// The block number when this epoch started.
207	pub epoch_start: BlockNumber,
208}
209
210/// A BoundedVec containing UnlockChunks
211pub type UnlockChunkList<T> = BoundedVec<
212	UnlockChunk<BalanceOf<T>, <T as Config>::EpochNumber>,
213	<T as Config>::MaxUnlockingChunks,
214>;
215
216/// Computes and returns the total token held in an UnlockChunkList.
217pub fn unlock_chunks_total<T: Config>(unlock_chunks: &UnlockChunkList<T>) -> BalanceOf<T> {
218	unlock_chunks
219		.iter()
220		.fold(Zero::zero(), |acc: BalanceOf<T>, chunk| acc.saturating_add(chunk.value))
221}
222
223/// Deletes thawed chunks
224/// Caller is responsible for updating free/locked balance on the token account.
225/// Returns: the total amount reaped from `unlocking`
226pub fn unlock_chunks_reap_thawed<T: Config>(
227	unlock_chunks: &mut UnlockChunkList<T>,
228	current_epoch: <T>::EpochNumber,
229) -> BalanceOf<T> {
230	let mut total_reaped: BalanceOf<T> = 0u32.into();
231	unlock_chunks.retain(|chunk| {
232		if current_epoch.ge(&chunk.thaw_at) {
233			total_reaped = total_reaped.saturating_add(chunk.value);
234			false
235		} else {
236			true
237		}
238	});
239	total_reaped
240}
241#[cfg(any(feature = "runtime-benchmarks", test))]
242#[allow(clippy::unwrap_used)]
243/// set unlock chunks with (balance, thaw_at).  Does not check BoundedVec limit.
244/// returns true on success, false on failure (?)
245/// For testing and benchmarks ONLY, note possible panic via BoundedVec::try_from + unwrap
246pub fn unlock_chunks_from_vec<T: Config>(chunks: &[(u32, u32)]) -> UnlockChunkList<T> {
247	let result: Vec<UnlockChunk<BalanceOf<T>, <T>::EpochNumber>> = chunks
248		.iter()
249		.map(|chunk| UnlockChunk { value: chunk.0.into(), thaw_at: chunk.1.into() })
250		.collect();
251	// CAUTION
252	BoundedVec::try_from(result).unwrap()
253}
254
255/// The information needed to track a Reward Era
256#[derive(
257	PartialEq,
258	Eq,
259	Clone,
260	Copy,
261	Default,
262	PartialOrd,
263	Encode,
264	Decode,
265	RuntimeDebug,
266	TypeInfo,
267	MaxEncodedLen,
268)]
269pub struct RewardEraInfo<RewardEra, BlockNumber>
270where
271	RewardEra: AtLeast32BitUnsigned + EncodeLike,
272	BlockNumber: AtLeast32BitUnsigned + EncodeLike,
273{
274	/// the index of this era
275	pub era_index: RewardEra,
276	/// the starting block of this era
277	pub started_at: BlockNumber,
278}
279
280/// A chunk of Reward Pool history items consists of a BoundedBTreeMap,
281/// RewardEra is the key and the total stake for the RewardPool is the value.
282/// the map has up to T::RewardPoolChunkLength items, however, the chunk storing the current era
283/// has only that one.
284#[derive(PartialEq, Eq, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
285#[scale_info(skip_type_params(T))]
286pub struct RewardPoolHistoryChunk<T: Config>(
287	BoundedBTreeMap<RewardEra, BalanceOf<T>, T::RewardPoolChunkLength>,
288);
289impl<T: Config> Default for RewardPoolHistoryChunk<T> {
290	fn default() -> Self {
291		Self::new()
292	}
293}
294
295impl<T: Config> Clone for RewardPoolHistoryChunk<T> {
296	fn clone(&self) -> Self {
297		Self(self.0.clone())
298	}
299}
300
301impl<T: Config> RewardPoolHistoryChunk<T> {
302	/// Constructs a new empty RewardPoolHistoryChunk
303	pub fn new() -> Self {
304		RewardPoolHistoryChunk(BoundedBTreeMap::new())
305	}
306
307	/// A wrapper for retrieving how much was provider_boosted in the given era
308	/// from the  BoundedBTreeMap
309	pub fn total_for_era(&self, reward_era: &RewardEra) -> Option<&BalanceOf<T>> {
310		self.0.get(reward_era)
311	}
312
313	/// returns the range of 		eras in this chunk
314	#[cfg(test)]
315	pub fn era_range(&self) -> (RewardEra, RewardEra) {
316		let zero_reward_era: RewardEra = Zero::zero();
317		let zero_balance: BalanceOf<T> = Zero::zero();
318		let (first, _vf) = self.0.first_key_value().unwrap_or((&zero_reward_era, &zero_balance));
319		let (last, _vl) = self.0.last_key_value().unwrap_or((&zero_reward_era, &zero_balance));
320		(*first, *last)
321	}
322
323	/// A wrapper for adding a new reward_era_entry to the BoundedBTreeMap
324	pub fn try_insert(
325		&mut self,
326		reward_era: RewardEra,
327		total: BalanceOf<T>,
328	) -> Result<Option<BalanceOf<T>>, (RewardEra, BalanceOf<T>)> {
329		self.0.try_insert(reward_era, total)
330	}
331
332	/// Get the earliest reward era stored in this BoundedBTreeMap
333	#[cfg(test)]
334	pub fn earliest_era(&self) -> Option<&RewardEra> {
335		if let Some((first_era, _first_total)) = self.0.first_key_value() {
336			return Some(first_era);
337		}
338		None
339	}
340
341	/// Is this chunk full?  It should always be yes once there is enough RewardPool history.
342	pub fn is_full(&self) -> bool {
343		self.0.len().eq(&(T::RewardPoolChunkLength::get() as usize))
344	}
345}
346
347/// A record of staked amounts for a complete RewardEra
348#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
349#[scale_info(skip_type_params(T))]
350pub struct ProviderBoostHistory<T: Config>(
351	BoundedBTreeMap<RewardEra, BalanceOf<T>, T::ProviderBoostHistoryLimit>,
352);
353
354impl<T: Config> Default for ProviderBoostHistory<T> {
355	fn default() -> Self {
356		Self::new()
357	}
358}
359
360impl<T: Config> ProviderBoostHistory<T> {
361	/// Constructs a new empty ProviderBoostHistory
362	pub fn new() -> Self {
363		ProviderBoostHistory(BoundedBTreeMap::new())
364	}
365
366	/// Adds `add_amount` to the entry for `reward_era`.
367	/// Updates entry, or creates a new entry if it does not exist
368	/// returns the total number of history items
369	pub fn add_era_balance(
370		&mut self,
371		reward_era: &RewardEra,
372		add_amount: &BalanceOf<T>,
373	) -> Option<usize> {
374		if let Some(entry) = self.0.get_mut(reward_era) {
375			// update
376			*entry = entry.saturating_add(*add_amount);
377		} else {
378			// insert
379			self.remove_oldest_entry_if_full(); // this guarantees a try_insert never fails
380			let current_staking_amount = self.get_last_staking_amount();
381			if self
382				.0
383				.try_insert(*reward_era, current_staking_amount.saturating_add(*add_amount))
384				.is_err()
385			{
386				return None;
387			};
388		}
389
390		Some(self.count())
391	}
392
393	/// Subtracts `subtract_amount` from the entry for `reward_era`. Zero values are still retained.
394	/// Returns None if there is no entry for the reward era.
395	/// Returns Some(0) if they unstaked everything and this is the only entry
396	/// Otherwise returns Some(history_count)
397	pub fn subtract_era_balance(
398		&mut self,
399		reward_era: &RewardEra,
400		subtract_amount: &BalanceOf<T>,
401	) -> Option<usize> {
402		if self.count().is_zero() {
403			return None;
404		};
405
406		let current_staking_amount = self.get_last_staking_amount();
407		if current_staking_amount.eq(subtract_amount) && self.count().eq(&1usize) {
408			// Should not get here unless rewards have all been claimed, and provider boost history was
409			// correctly updated. This && condition is to protect stakers against loss of rewards in the
410			// case of some bug with payouts and boost history.
411			return Some(0usize);
412		}
413
414		if let Some(entry) = self.0.get_mut(reward_era) {
415			*entry = entry.saturating_sub(*subtract_amount);
416		} else {
417			self.remove_oldest_entry_if_full();
418			if self
419				.0
420				.try_insert(*reward_era, current_staking_amount.saturating_sub(*subtract_amount))
421				.is_err()
422			{
423				return None;
424			}
425		}
426		Some(self.count())
427	}
428
429	/// A wrapper for the key/value retrieval of the BoundedBTreeMap (only used in tests)
430	#[cfg(test)]
431	pub(crate) fn get_entry_for_era(&self, reward_era: &RewardEra) -> Option<&BalanceOf<T>> {
432		self.0.get(reward_era)
433	}
434
435	/// Returns how much was staked during the given era, even if there is no explicit entry for that era.
436	/// If there is no history entry for `reward_era`, returns the next earliest entry's staking balance.
437	///
438	/// Note there is no sense of what the current era is; subsequent calls could return a different result
439	/// if 'reward_era' is the current era and there has been a boost or unstake.
440	pub(crate) fn get_amount_staked_for_era(&self, reward_era: &RewardEra) -> BalanceOf<T> {
441		// this gives an ordered-by-key Iterator
442		let bmap_iter = self.0.iter();
443		let mut eligible_amount: BalanceOf<T> = Zero::zero();
444		for (era, balance) in bmap_iter {
445			if era.eq(reward_era) {
446				return *balance;
447			}
448			// there was a boost or unstake in this era.
449			else if era.gt(reward_era) {
450				return eligible_amount;
451			} // eligible_amount has been staked through reward_era
452			eligible_amount = *balance;
453		}
454		eligible_amount
455	}
456
457	/// Returns the number of history items
458	pub fn count(&self) -> usize {
459		self.0.len()
460	}
461
462	fn remove_oldest_entry_if_full(&mut self) {
463		if self.is_full() {
464			// compiler errors with unwrap
465			if let Some((earliest_key, _earliest_val)) = self.0.first_key_value() {
466				self.0.remove(&earliest_key.clone());
467			}
468		}
469	}
470
471	fn get_last_staking_amount(&self) -> BalanceOf<T> {
472		// compiler errors with unwrap
473		if let Some((_last_key, last_value)) = self.0.last_key_value() {
474			return *last_value;
475		};
476		Zero::zero()
477	}
478
479	/// Returns the earliest RewardEra in the BoundedBTreeMap, or None if the map is empty.
480	pub fn get_earliest_reward_era(&self) -> Option<&RewardEra> {
481		self.0.first_key_value().map(|(key, _value)| key)
482	}
483
484	fn is_full(&self) -> bool {
485		self.count().eq(&(T::ProviderBoostHistoryLimit::get() as usize))
486	}
487}
488
489/// Struct with utilities for storing and updating unlock chunks
490#[derive(Debug, TypeInfo, PartialEqNoBound, EqNoBound, Clone, Decode, Encode, MaxEncodedLen)]
491#[scale_info(skip_type_params(T))]
492pub struct RetargetInfo<T: Config> {
493	/// How many times the account has retargeted this RewardEra
494	pub retarget_count: u32,
495	/// The last RewardEra they retargeted
496	pub last_retarget_at: RewardEra,
497	_marker: PhantomData<T>,
498}
499
500impl<T: Config> Default for RetargetInfo<T> {
501	fn default() -> Self {
502		Self { retarget_count: 0u32, last_retarget_at: Zero::zero(), _marker: Default::default() }
503	}
504}
505
506impl<T: Config> RetargetInfo<T> {
507	/// Constructor
508	pub fn new(retarget_count: u32, last_retarget_at: RewardEra) -> Self {
509		Self { retarget_count, last_retarget_at, _marker: Default::default() }
510	}
511	/// Increment retarget count and return Some() or
512	/// If there are too many, return None
513	pub fn update(&mut self, current_era: RewardEra) -> Option<()> {
514		let max_retargets = T::MaxRetargetsPerRewardEra::get();
515		if self.retarget_count.ge(&max_retargets) && self.last_retarget_at.eq(&current_era) {
516			return None;
517		}
518		if self.last_retarget_at.lt(&current_era) {
519			self.last_retarget_at = current_era;
520			self.retarget_count = 1;
521		} else {
522			self.retarget_count = self.retarget_count.saturating_add(1u32);
523		}
524		Some(())
525	}
526}
527
528/// A trait that provides the Economic Model for Provider Boosting.
529pub trait ProviderBoostRewardsProvider<T: Config> {
530	/// The type for currency
531	type Balance;
532
533	/// Return the size of the reward pool using the current economic model
534	fn reward_pool_size(total_staked: BalanceOf<T>) -> BalanceOf<T>;
535
536	/// Calculate the reward for a single era.  We don't care about the era number,
537	/// just the values.
538	fn era_staking_reward(
539		era_amount_staked: BalanceOf<T>, // how much individual staked for a specific era
540		era_total_staked: BalanceOf<T>,  // how much everyone staked for the era
541		era_reward_pool_size: BalanceOf<T>, // how much token in the reward pool that era
542	) -> BalanceOf<T>;
543
544	/// Return the effective amount when staked for a Provider Boost
545	/// The amount is multiplied by a factor > 0 and < 1.
546	fn capacity_boost(amount: BalanceOf<T>) -> BalanceOf<T>;
547}