referrerpolicy=no-referrer-when-downgrade

pallet_staking_async/
reward.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Era reward management.
19//!
20//! Manages the lifecycle of era reward pot accounts: creation, funding
21//! via snapshot from the general DAP pot, and draining of expired eras.
22//!
23//! Era pots are backed by a rotating pool of `POT_POOL_SIZE` accounts
24//! addressed by `era % POT_POOL_SIZE`. Once created, a slot's account is kept
25//! alive forever — at the end of each era's history window, its remaining
26//! balance is drained to [`crate::Config::UnclaimedRewardHandler`] but the
27//! provider reference is retained. A future era that reuses the same slot
28//! finds an existing zero-balance account and snapshots into it. This bounds
29//! the storage footprint contributed by era pots to a constant.
30
31use crate::*;
32use frame_support::{
33	defensive,
34	traits::{
35		fungible::{Balanced, Inspect, Mutate},
36		tokens::{Fortitude, Precision, Preservation},
37		OnUnbalanced,
38	},
39};
40use sp_runtime::{
41	traits::{AtLeast32BitUnsigned, Zero},
42	Perbill,
43};
44use sp_staking::EraIndex;
45
46/// Allocation breakdown of era-end rewards.
47#[derive(
48	Debug,
49	Clone,
50	Copy,
51	PartialEq,
52	Eq,
53	codec::Encode,
54	codec::Decode,
55	codec::DecodeWithMemTracking,
56	scale_info::TypeInfo,
57)]
58pub struct EraRewardAllocation<Balance> {
59	pub staker_rewards: Balance,
60	pub validator_incentive: Balance,
61}
62
63/// Manager for era reward pot lifecycle.
64pub struct EraRewardManager<T: Config>(core::marker::PhantomData<T>);
65
66impl<T: Config> EraRewardManager<T> {
67	/// Ensures the era pot account for `(era, kind)` exists by holding a provider
68	/// reference. Idempotent: if the slot's account is already provided (because a
69	/// previous era reused it), this is a no-op.
70	///
71	/// Should only be called in non-minting mode (`DisableMinting = true`).
72	pub(crate) fn create(era: EraIndex, kind: RewardKind) -> T::AccountId {
73		debug_assert!(
74			T::DisableMinting::get(),
75			"Era pots should only be created when DisableMinting is true"
76		);
77		let pot_account = T::RewardPots::pot_account(RewardPot::Era(era, kind));
78		if frame_system::Pallet::<T>::providers(&pot_account) == 0 {
79			frame_system::Pallet::<T>::inc_providers(&pot_account);
80		}
81		pot_account
82	}
83
84	/// Snapshots the general reward pots into era-specific pots.
85	///
86	/// DAP drips inflation continuously into the general pots. At era boundary,
87	/// this transfers the accumulated balances (minus ED) into era pots.
88	pub(crate) fn snapshot_era_rewards(era: EraIndex) -> EraRewardAllocation<BalanceOf<T>> {
89		let staker_era_pot = Self::create(era, RewardKind::StakerRewards);
90		let incentive_era_pot = Self::create(era, RewardKind::ValidatorSelfStake);
91
92		let general_staker_pot =
93			T::RewardPots::pot_account(RewardPot::General(RewardKind::StakerRewards));
94		let general_incentive_pot =
95			T::RewardPots::pot_account(RewardPot::General(RewardKind::ValidatorSelfStake));
96
97		// Leave ED in the general pots to keep them alive.
98		let staker_balance = T::Currency::reducible_balance(
99			&general_staker_pot,
100			Preservation::Preserve,
101			Fortitude::Polite,
102		);
103		let incentive_balance = T::Currency::reducible_balance(
104			&general_incentive_pot,
105			Preservation::Preserve,
106			Fortitude::Polite,
107		);
108
109		let actual_staker = if !staker_balance.is_zero() {
110			match T::Currency::transfer(
111				&general_staker_pot,
112				&staker_era_pot,
113				staker_balance,
114				Preservation::Preserve,
115			) {
116				Ok(_) => staker_balance,
117				Err(e) => {
118					log!(error, "Era {:?}: staker reward transfer failed: {:?}", era, e);
119					defensive!("Failed to transfer staker rewards to era pot");
120					Zero::zero()
121				},
122			}
123		} else {
124			Zero::zero()
125		};
126
127		let actual_incentive = if !incentive_balance.is_zero() {
128			match T::Currency::transfer(
129				&general_incentive_pot,
130				&incentive_era_pot,
131				incentive_balance,
132				Preservation::Preserve,
133			) {
134				Ok(_) => incentive_balance,
135				Err(e) => {
136					log!(error, "Era {:?}: validator incentive transfer failed: {:?}", era, e);
137					defensive!("Failed to transfer validator incentive to era pot");
138					Zero::zero()
139				},
140			}
141		} else {
142			Zero::zero()
143		};
144
145		log!(
146			info,
147			"Era {:?}: snapshotted staker_rewards={:?}, validator_incentive={:?}",
148			era,
149			actual_staker,
150			actual_incentive
151		);
152
153		EraRewardAllocation { staker_rewards: actual_staker, validator_incentive: actual_incentive }
154	}
155
156	/// Drains an era pot's remaining balance to the unclaimed reward handler.
157	///
158	/// The pot account itself is kept alive (provider retained) so the same slot
159	/// can be reused by a future era. No-op if the pot was never created (e.g.
160	/// the era ran in legacy minting mode).
161	pub(crate) fn drain(era: EraIndex, kind: RewardKind) {
162		let pot_account = T::RewardPots::pot_account(RewardPot::Era(era, kind));
163
164		// Skip if pot was never created (legacy mode doesn't create pots).
165		if frame_system::Pallet::<T>::providers(&pot_account) == 0 {
166			return;
167		}
168
169		let remaining = T::Currency::balance(&pot_account);
170
171		if remaining.is_zero() {
172			return;
173		}
174
175		match T::Currency::withdraw(
176			&pot_account,
177			remaining,
178			Precision::BestEffort,
179			Preservation::Expendable,
180			Fortitude::Force,
181		) {
182			Ok(credit) => {
183				T::UnclaimedRewardHandler::on_unbalanced(credit);
184				log!(
185					debug,
186					"Drained {:?} unclaimed rewards from era {:?} {:?} pot",
187					remaining,
188					era,
189					kind
190				);
191			},
192			Err(e) => {
193				defensive!("Failed to withdraw unclaimed rewards from era pot");
194				log!(
195					error,
196					"Era {:?} {:?}: unclaimed reward withdrawal failed: {:?}",
197					era,
198					kind,
199					e
200				);
201			},
202		}
203	}
204
205	/// Whether the slot backing this era's staker reward pot exists.
206	///
207	/// Because slots are reused across eras (rotating pool), this returns
208	/// `true` for an era as long as *some* era mapping to the same slot
209	/// has created the account.
210	#[cfg(any(test, feature = "try-runtime"))]
211	pub(crate) fn has_staker_rewards_pot(era: EraIndex) -> bool {
212		let pot = T::RewardPots::pot_account(RewardPot::Era(era, RewardKind::StakerRewards));
213		frame_system::Pallet::<T>::providers(&pot) > 0
214	}
215
216	/// Cleans up all pot accounts for a given era by draining their balances.
217	///
218	/// Pot accounts are kept alive for reuse by a future era at the same slot.
219	pub(crate) fn cleanup_era(era: EraIndex) {
220		Self::drain(era, RewardKind::StakerRewards);
221		Self::drain(era, RewardKind::ValidatorSelfStake);
222	}
223}
224
225/// Default implementation of the staker reward calculator.
226///
227/// Commission-based split: validator gets commission + proportional stake share,
228/// nominators get the rest. Incentive weight returns zero (no incentive curve).
229pub struct DefaultStakerRewardCalculator<T>(core::marker::PhantomData<T>);
230
231impl<T: Config> sp_staking::StakerRewardCalculator<BalanceOf<T>>
232	for DefaultStakerRewardCalculator<T>
233where
234	BalanceOf<T>: Into<u128> + From<u128>,
235{
236	fn calculate_validator_incentive_weight(self_stake: BalanceOf<T>) -> BalanceOf<T> {
237		let optimum = OptimumSelfStake::<T>::get();
238		let cap = HardCapSelfStake::<T>::get();
239		let slope_factor = SelfStakeSlopeFactor::<T>::get();
240
241		incentive_weight::<BalanceOf<T>>(self_stake, optimum, cap, slope_factor)
242	}
243
244	fn calculate_staker_reward(
245		validator_total_reward: BalanceOf<T>,
246		validator_commission: Perbill,
247		validator_own_stake: BalanceOf<T>,
248		total_exposure: BalanceOf<T>,
249	) -> sp_staking::StakerRewardResult<BalanceOf<T>> {
250		let validator_commission_payout = validator_commission.mul_floor(validator_total_reward);
251		let leftover = validator_total_reward.saturating_sub(validator_commission_payout);
252		let validator_exposure_part = Perbill::from_rational(validator_own_stake, total_exposure);
253		let validator_staking_payout = validator_exposure_part.mul_floor(leftover);
254		let validator_payout = validator_staking_payout.saturating_add(validator_commission_payout);
255		let nominator_payout = leftover.saturating_sub(validator_staking_payout);
256
257		// Validator and nominator payout is exactly same as total reward.
258		debug_assert_eq!(validator_payout + nominator_payout, validator_total_reward);
259
260		sp_staking::StakerRewardResult { validator_payout, nominator_payout }
261	}
262}
263
264/// Piecewise sqrt-based incentive weight function.
265///
266/// - Below optimum: `w(s) = √s`
267/// - Between optimum and cap: `w(s) = √(T + k² × (s - T))`
268/// - Above cap: plateau at `w(cap)`
269fn incentive_weight<Balance>(
270	self_stake: Balance,
271	optimum: Balance,
272	cap: Balance,
273	slope_factor: Perbill,
274) -> Balance
275where
276	Balance: AtLeast32BitUnsigned + Copy + Into<u128> + From<u128>,
277{
278	debug_assert!(optimum <= cap, "config invariant: optimum must be <= cap");
279
280	if self_stake.is_zero() {
281		return Balance::zero();
282	}
283
284	if optimum.is_zero() && cap.is_zero() {
285		return Balance::zero();
286	}
287
288	let self_stake_u128: u128 = self_stake.into();
289	let optimum_u128: u128 = optimum.into();
290	let cap_u128: u128 = cap.into();
291
292	let weight_u128 = if self_stake <= optimum {
293		sp_arithmetic::helpers_128bit::sqrt(self_stake_u128)
294	} else if self_stake <= cap {
295		let k_squared = slope_factor.square();
296		let excess = self_stake_u128.saturating_sub(optimum_u128);
297		let arg = optimum_u128.saturating_add(k_squared.mul_floor(excess));
298		sp_arithmetic::helpers_128bit::sqrt(arg)
299	} else {
300		let k_squared = slope_factor.square();
301		let excess = cap_u128.saturating_sub(optimum_u128);
302		let arg = optimum_u128.saturating_add(k_squared.mul_floor(excess));
303		sp_arithmetic::helpers_128bit::sqrt(arg)
304	};
305
306	Balance::from(weight_u128)
307}
308
309#[cfg(test)]
310mod tests {
311	use super::*;
312	use sp_runtime::Perbill;
313
314	type Balance = u128;
315
316	#[test]
317	fn incentive_weight_zero_self_stake() {
318		assert_eq!(
319			incentive_weight::<Balance>(0, 100_000, 500_000, Perbill::from_rational(1u32, 2u32)),
320			0
321		);
322	}
323
324	#[test]
325	fn incentive_weight_config_not_set() {
326		// Both optimum and cap are zero (config never set) -> disabled.
327		assert_eq!(
328			incentive_weight::<Balance>(100_000, 0, 0, Perbill::from_rational(1u32, 2u32)),
329			0
330		);
331	}
332
333	#[test]
334	fn incentive_weight_optimum_zero_cap_set() {
335		// optimum = 0, cap > 0: dampened-growth zone from 0 up to cap.
336		let slope = Perbill::from_rational(1u32, 2u32);
337		// self_stake below cap: w(s) = √(0 + 0.25·s) = √(s/4).
338		// s = 400_000 -> √100_000 ≈ 316.
339		assert_eq!(incentive_weight::<Balance>(400_000, 0, 500_000, slope), 316);
340		// Same self-stake with a positive optimum yields higher weight
341		assert_eq!(incentive_weight::<Balance>(400_000, 100_000, 500_000, slope), 418);
342		// Above cap plateaus at √(0.25·cap) = √125_000 ≈ 353.
343		assert_eq!(incentive_weight::<Balance>(1_000_000, 0, 500_000, slope), 353);
344	}
345
346	#[test]
347	fn incentive_weight_below_optimum() {
348		// √10_000 = 100
349		assert_eq!(
350			incentive_weight::<Balance>(
351				10_000,
352				100_000,
353				500_000,
354				Perbill::from_rational(1u32, 2u32)
355			),
356			100
357		);
358	}
359
360	#[test]
361	fn incentive_weight_at_optimum() {
362		// √100_000 ≈ 316
363		assert_eq!(
364			incentive_weight::<Balance>(
365				100_000,
366				100_000,
367				500_000,
368				Perbill::from_rational(1u32, 2u32)
369			),
370			316
371		);
372	}
373
374	#[test]
375	fn incentive_weight_between_optimum_and_cap() {
376		// √(100k + 0.25 × 200k) = √150k ≈ 387
377		assert_eq!(
378			incentive_weight::<Balance>(
379				300_000,
380				100_000,
381				500_000,
382				Perbill::from_rational(1u32, 2u32)
383			),
384			387
385		);
386	}
387
388	#[test]
389	fn incentive_weight_at_cap() {
390		// √(100k + 0.25 × 400k) = √200k ≈ 447
391		assert_eq!(
392			incentive_weight::<Balance>(
393				500_000,
394				100_000,
395				500_000,
396				Perbill::from_rational(1u32, 2u32)
397			),
398			447
399		);
400	}
401
402	#[test]
403	fn incentive_weight_plateau_above_cap() {
404		let at_cap = incentive_weight::<Balance>(
405			500_000,
406			100_000,
407			500_000,
408			Perbill::from_rational(1u32, 2u32),
409		);
410		let above = incentive_weight::<Balance>(
411			1_000_000,
412			100_000,
413			500_000,
414			Perbill::from_rational(1u32, 2u32),
415		);
416		assert_eq!(at_cap, above);
417	}
418
419	#[test]
420	fn incentive_weight_monotonically_increasing_below_cap() {
421		let slope = Perbill::from_rational(1u32, 2u32);
422		let w1 = incentive_weight::<Balance>(50_000, 100_000, 500_000, slope);
423		let w2 = incentive_weight::<Balance>(100_000, 100_000, 500_000, slope);
424		let w3 = incentive_weight::<Balance>(200_000, 100_000, 500_000, slope);
425		let w4 = incentive_weight::<Balance>(400_000, 100_000, 500_000, slope);
426		assert!(w1 < w2 && w2 < w3 && w3 < w4);
427	}
428
429	#[test]
430	fn incentive_weight_different_slope_factors() {
431		let self_stake = 300_000;
432		let w_025 = incentive_weight::<Balance>(
433			self_stake,
434			100_000,
435			500_000,
436			Perbill::from_rational(1u32, 4u32),
437		);
438		let w_050 = incentive_weight::<Balance>(
439			self_stake,
440			100_000,
441			500_000,
442			Perbill::from_rational(1u32, 2u32),
443		);
444		let w_075 = incentive_weight::<Balance>(
445			self_stake,
446			100_000,
447			500_000,
448			Perbill::from_rational(3u32, 4u32),
449		);
450		assert!(w_025 < w_050 && w_050 < w_075);
451	}
452
453	#[test]
454	fn incentive_weight_slope_factor_zero_plateaus_at_optimum() {
455		// k=0 -> immediate plateau at optimum (no growth beyond T).
456		let at_optimum = incentive_weight::<Balance>(100_000, 100_000, 500_000, Perbill::zero());
457		let above_optimum = incentive_weight::<Balance>(300_000, 100_000, 500_000, Perbill::zero());
458		assert_eq!(at_optimum, above_optimum);
459	}
460
461	#[test]
462	fn incentive_weight_slope_factor_one_no_discouragement() {
463		// k=1 -> no discouragement above T (same curve as below T).
464		let at_optimum = incentive_weight::<Balance>(100_000, 100_000, 500_000, Perbill::one());
465		let at_cap = incentive_weight::<Balance>(500_000, 100_000, 500_000, Perbill::one());
466		// sqrt(100_000) = 316, sqrt(500_000) = 707
467		assert_eq!(at_optimum, 316);
468		assert_eq!(at_cap, 707);
469	}
470
471	#[test]
472	fn incentive_weight_optimum_equals_cap() {
473		// When T == C, the middle segment vanishes -- plateau immediately at T.
474		let slope = Perbill::from_rational(1u32, 2u32);
475		let at_boundary = incentive_weight::<Balance>(100_000, 100_000, 100_000, slope);
476		let above = incentive_weight::<Balance>(200_000, 100_000, 100_000, slope);
477		assert_eq!(at_boundary, above);
478		assert_eq!(at_boundary, 316); // sqrt(100_000)
479	}
480}