Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,10 @@ impl<T: frame_system::Config> pallet_staking_async::WeightInfo for WeightInfo<T>
.saturating_add(T::DbWeight::get().reads(1))
.saturating_add(T::DbWeight::get().writes(1))
}
fn set_validator_self_stake_incentive_config() -> Weight {
// TODO(ank4n) run bench
T::DbWeight::get().reads_writes(2, 3)
}
/// Storage: `System::Account` (r:1 w:0)
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
/// Storage: `Staking::VirtualStakers` (r:1 w:0)
Expand Down
17 changes: 17 additions & 0 deletions prdoc/pr_11651.prdoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
title: "Validator self-stake incentive curve (non-vested)"
doc:
- audience: Runtime Dev
description: |-
Adds a separate validator incentive reward track funded from a second DAP budget pot.
Each validator's share is determined by a sqrt-based piecewise weight function of their
self-stake, with governance-configurable parameters (optimum, cap, slope factor).
Payout is a direct liquid transfer from the era incentive pot.

New extrinsic: `set_validator_self_stake_incentive_config` (AdminOrigin).
crates:
- name: pallet-staking-async
bump: major
- name: asset-hub-westend-runtime
bump: patch
- name: pallet-ahm-test
bump: patch
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ impl pallet_staking_async::WeightInfo for StakingAsyncWeightInfo {
fn set_max_commission() -> Weight {
unreachable!()
}
fn set_validator_self_stake_incentive_config() -> Weight {
unreachable!()
}
fn restore_ledger() -> Weight {
unreachable!()
}
Expand Down
26 changes: 25 additions & 1 deletion substrate/frame/staking-async/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,11 +567,13 @@ impl<T: Config> Contains<T::AccountId> for AllStakers<T> {
}
}

/// Identifies the era pot account for staker rewards.
/// Identifies different types of era pot accounts for reward distribution.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode, TypeInfo)]
pub enum EraPotType {
/// Pot for staker rewards (nominators + validators).
StakerRewards,
/// Pot for validator self-stake incentive.
ValidatorSelfStake,
}

/// Trait for generating era pot account IDs.
Expand Down Expand Up @@ -605,6 +607,7 @@ where
fn era_pot_account(era: EraIndex, pot_type: EraPotType) -> AccountId {
let pot_type_offset = match pot_type {
EraPotType::StakerRewards => 0,
EraPotType::ValidatorSelfStake => 1,
};
AccountId::from(100_000 + (era as u64 * 10) + pot_type_offset)
}
Expand All @@ -618,6 +621,8 @@ where
pub enum GeneralPotType {
/// General pot for staker rewards.
StakerRewards,
/// General pot for validator self-stake incentive.
ValidatorIncentive,
}

/// Trait that provides general (non-era-specific) pot accounts.
Expand All @@ -644,6 +649,7 @@ where
fn general_pot_account(pot_type: GeneralPotType) -> AccountId {
match pot_type {
GeneralPotType::StakerRewards => AccountId::from(200_000u64),
GeneralPotType::ValidatorIncentive => AccountId::from(200_001u64),
}
}
}
Expand All @@ -666,6 +672,24 @@ where
}
}

/// Budget recipient for validator self-stake incentive.
///
/// Exposes the general validator incentive pot so DAP can drip inflation into it.
pub struct ValidatorIncentiveRecipient<G>(core::marker::PhantomData<G>);

impl<AccountId, G> sp_staking::budget::BudgetRecipient<AccountId> for ValidatorIncentiveRecipient<G>
where
G: GeneralPotAccountProvider<AccountId>,
{
fn budget_key() -> sp_staking::budget::BudgetKey {
sp_staking::budget::BudgetKey::truncate_from(b"validator_incentive".to_vec())
}

fn pot_account() -> AccountId {
G::general_pot_account(GeneralPotType::ValidatorIncentive)
}
}

/// A smart type to determine the [`Config::PlanningEraOffset`], given:
///
/// * Expected relay session duration, `RS`
Expand Down
16 changes: 15 additions & 1 deletion substrate/frame/staking-async/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,8 @@ impl pallet_dap::Config for Test {
type Currency = Balances;
type PalletId = DapPalletId;
type IssuanceCurve = OneTokenPerMillisecond;
type BudgetRecipients = (Dap, StakerRewardRecipient<SequentialTest>);
type BudgetRecipients =
(Dap, StakerRewardRecipient<SequentialTest>, ValidatorIncentiveRecipient<SequentialTest>);
type Time = MockTime;
type IssuanceCadence = TestIssuanceCadence;
type MaxElapsedPerDrip = TestMaxElapsedPerDrip;
Expand All @@ -486,6 +487,14 @@ pub(crate) fn staker_reward_key() -> sp_staking::budget::BudgetKey {
<StakerRewardRecipient<SequentialTest> as BudgetRecipient<AccountId>>::budget_key()
}

pub(crate) fn validator_incentive_key() -> sp_staking::budget::BudgetKey {
<ValidatorIncentiveRecipient<SequentialTest> as BudgetRecipient<AccountId>>::budget_key()
}

pub fn general_incentive_pot() -> AccountId {
SequentialTest::general_pot_account(GeneralPotType::ValidatorIncentive)
}

pub(crate) fn buffer_key() -> sp_staking::budget::BudgetKey {
<Dap as BudgetRecipient<AccountId>>::budget_key()
}
Expand Down Expand Up @@ -808,6 +817,11 @@ impl ExtBuilder {
ed,
)
.expect("mint general staker pot");
<Balances as frame_support::traits::fungible::Mutate<_>>::mint_into(
&general_incentive_pot(),
ed,
)
.expect("mint general incentive pot");
let dap_buffer =
<pallet_dap::Pallet<Test> as BudgetRecipient<AccountId>>::pot_account();
<Balances as frame_support::traits::fungible::Mutate<_>>::mint_into(
Expand Down
111 changes: 107 additions & 4 deletions substrate/frame/staking-async/src/pallet/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ use frame_support::{
dispatch::WithPostDispatchInfo,
pallet_prelude::*,
traits::{
Defensive, DefensiveSaturating, Get, Imbalance, InspectLockableCurrency, LockableCurrency,
OnUnbalanced,
fungible::Mutate as FunMutate, tokens::Preservation, Defensive, DefensiveSaturating, Get,
Imbalance, InspectLockableCurrency, LockableCurrency, OnUnbalanced,
},
weights::Weight,
StorageDoubleMap,
Expand Down Expand Up @@ -425,6 +425,14 @@ impl<T: Config> Pallet<T> {
let validator_staker_payout_for_page =
page_stake_part.mul_floor(reward_split.validator_payout);

// Pay validator incentive bonus from the separate incentive pot.
// Emits `ValidatorIncentivePaid` event inside `transfer_validator_incentive`.
if let Some(incentive) =
Self::calculate_validator_incentive_for_page(era, &stash, page_stake_part)
{
Self::transfer_validator_incentive(era, &stash, incentive);
}

Self::deposit_event(Event::<T>::PayoutStarted {
era_index: era,
validator_stash: stash.clone(),
Expand Down Expand Up @@ -568,8 +576,6 @@ impl<T: Config> Pallet<T> {
stash: &T::AccountId,
amount: BalanceOf<T>,
) -> Option<(BalanceOf<T>, RewardDestination<T::AccountId>)> {
use frame_support::traits::{fungible::Mutate as FunMutate, tokens::Preservation};

if amount.is_zero() {
return None;
}
Expand Down Expand Up @@ -654,6 +660,103 @@ impl<T: Config> Pallet<T> {
maybe_imbalance.map(|imbalance| (imbalance, dest))
}

/// Calculate the validator incentive amount for a single page.
fn calculate_validator_incentive_for_page(
era: EraIndex,
stash: &T::AccountId,
page_stake_part: Perbill,
) -> Option<BalanceOf<T>> {
let era_incentive_budget = Eras::<T>::get_validator_incentive_budget(era);
if era_incentive_budget.is_zero() {
return None;
}

let (validator_weight, total_weight) = match (
ErasValidatorIncentiveWeight::<T>::get(era, stash),
ErasSumValidatorIncentiveWeight::<T>::get(era),
) {
(Some(w), t) => (w, t),
_ => return None,
};

if total_weight.is_zero() {
log!(warn, "Total validator weight is zero but pot allocation exists for era {}", era);
Self::deposit_event(Event::<T>::Unexpected(
UnexpectedKind::ValidatorIncentiveWeightMismatch { era },
));
return None;
}

if validator_weight.is_zero() {
return None;
}

let validator_weight_part = Perbill::from_rational(validator_weight, total_weight);
let validator_total_incentive = validator_weight_part.mul_floor(era_incentive_budget);
let validator_incentive_for_page = page_stake_part.mul_floor(validator_total_incentive);

if validator_incentive_for_page.is_zero() {
return None;
}

Some(validator_incentive_for_page)
}

/// Transfer validator incentive from era pot to the validator's payout account.
///
/// This is a direct liquid transfer. Future PRs may introduce vesting via a trait.
fn transfer_validator_incentive(
era: EraIndex,
stash: &T::AccountId,
amount: BalanceOf<T>,
) -> BalanceOf<T> {
let (dest, payout_account) = match Self::payee(Stash(stash.clone())) {
Some(d) if !matches!(d, RewardDestination::None) => {
match Self::payout_account_for_dest(stash, &d) {
Some(account) => (d, account),
None => {
defensive!("Unable to determine payout account for destination");
return Zero::zero();
},
}
},
_ => {
defensive!("Validator missing payee");
Self::deposit_event(Event::<T>::Unexpected(
UnexpectedKind::ValidatorMissingPayee { era },
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to add a test checking for ValidatorMissingPayee event?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since it panics in the test, I can't assert on event, can I? But added a test for panic (so practically the same thing)

));
return Zero::zero();
},
};

let incentive_pot = T::EraPots::era_pot_account(era, crate::EraPotType::ValidatorSelfStake);

match T::Currency::transfer(
&incentive_pot,
&payout_account,
amount,
Preservation::Expendable,
) {
Ok(_) => {
Self::deposit_event(Event::<T>::ValidatorIncentivePaid {
era,
validator_stash: stash.clone(),
dest,
amount,
});
amount
},
Err(e) => {
log!(warn, "Failed to transfer liquid incentive: {:?}", e);
defensive!("Validator incentive liquid transfer failed");
Self::deposit_event(Event::<T>::Unexpected(
UnexpectedKind::ValidatorIncentiveTransferFailed { era },
));
Zero::zero()
},
}
}

/// Chill a stash account.
pub(crate) fn chill_stash(stash: &T::AccountId) {
let chilled_as_validator = Self::do_remove_validator(stash);
Expand Down
Loading
Loading