Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/reward pool refactor #1976 #2005

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
27 changes: 14 additions & 13 deletions pallets/capacity/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,8 @@ pub fn set_era_and_reward_pool_at_block<T: Config>(
) {
let era_info: RewardEraInfo<T::RewardEra, BlockNumberFor<T>> =
RewardEraInfo { era_index, started_at };
let total_reward_pool: BalanceOf<T> =
T::MinimumStakingAmount::get().saturating_add(1_100u32.into());
CurrentEraInfo::<T>::set(era_info);
let pool_info: RewardPoolInfo<BalanceOf<T>> = RewardPoolInfo {
total_staked_token,
total_reward_pool,
unclaimed_balance: total_reward_pool,
};
ProviderBoostRewardPool::<T>::insert(era_index, pool_info);
CurrentEraProviderBoostTotal::<T>::set(total_staked_token)
}

// caller stakes the given amount to the given target
Expand Down Expand Up @@ -93,6 +86,18 @@ fn fill_unlock_chunks<T: Config>(caller: &T::AccountId, count: u32) {
UnstakeUnlocks::<T>::set(caller, Some(unlocking));
}

fn fill_reward_pool_chunks<T: Config>() {
let chunk_len = T::RewardPoolChunkLength::get();
let chunks = T::ProviderBoostHistoryLimit::get() / (chunk_len);
for i in 0..chunks {
let mut new_chunk = RewardPoolHistoryChunk::<T>::new();
for j in 0..chunk_len {
assert_ok!(new_chunk.try_insert((i + 1u32).into(), (1000u32 * (j + i)).into()));
}
ProviderBoostRewardPools::<T>::set(i, Some(new_chunk));
}
}

benchmarks! {
stake {
let caller: T::AccountId = create_funded_account::<T>("account", SEED, 105u32);
Expand Down Expand Up @@ -145,11 +150,7 @@ benchmarks! {

let current_era: T::RewardEra = (history_limit + 1u32).into();
CurrentEraInfo::<T>::set(RewardEraInfo{ era_index: current_era, started_at });

for i in 0..history_limit {
let era: T::RewardEra = i.into();
ProviderBoostRewardPool::<T>::insert(era, RewardPoolInfo { total_staked_token, total_reward_pool, unclaimed_balance});
}
fill_reward_pool_chunks::<T>();
}: {
Capacity::<T>::start_new_reward_era_if_needed(current_block);
} verify {
Expand Down
209 changes: 142 additions & 67 deletions pallets/capacity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@
rustdoc::invalid_codeblock_attributes,
missing_docs
)]
use sp_std::ops::{Add, Mul};

use sp_std::ops::Mul;

use frame_support::{
ensure,
Expand Down Expand Up @@ -188,6 +189,8 @@ pub mod pallet {
+ EncodeLike
+ Into<BalanceOf<Self>>
+ Into<BlockNumberFor<Self>>
+ Into<u32>
+ EncodeLike<u32>
+ TypeInfo;

/// The number of blocks in a RewardEra
Expand All @@ -214,6 +217,10 @@ pub mod pallet {
/// the percentage cap per era of an individual Provider Boost reward
#[pallet::constant]
type RewardPercentCap: Get<Permill>;

/// The number of chunks of Reward Pool history we expect to store
#[pallet::constant]
type RewardPoolChunkLength: Get<u32>;
wilwade marked this conversation as resolved.
Show resolved Hide resolved
}

/// Storage for keeping a ledger of staked token amounts for accounts.
Expand Down Expand Up @@ -275,29 +282,36 @@ pub mod pallet {
pub type UnstakeUnlocks<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, UnlockChunkList<T>>;

/// stores how many times an account has retargeted, and when it last retargeted.
#[pallet::storage]
#[pallet::getter(fn get_retargets_for)]
pub type Retargets<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RetargetInfo<T>>;

/// Information about the current reward era. Checked every block.
#[pallet::storage]
#[pallet::whitelist_storage]
#[pallet::getter(fn get_current_era)]
pub type CurrentEraInfo<T: Config> =
StorageValue<_, RewardEraInfo<T::RewardEra, BlockNumberFor<T>>, ValueQuery>;

/// Reward Pool history
/// Reward Pool history is divided into chunks of size RewardPoolChunkLength.
/// ProviderBoostHistoryLimit is the total number of items, the key is the
/// chunk number.
#[pallet::storage]
#[pallet::getter(fn get_reward_pool_for_era)]
pub type ProviderBoostRewardPool<T: Config> =
CountedStorageMap<_, Twox64Concat, T::RewardEra, RewardPoolInfo<BalanceOf<T>>>;
#[pallet::getter(fn get_reward_pool_chunk)]
pub type ProviderBoostRewardPools<T: Config> =
StorageMap<_, Twox64Concat, u32, RewardPoolHistoryChunk<T>>;
wilwade marked this conversation as resolved.
Show resolved Hide resolved

/// How much is staked this era
#[pallet::storage]
pub type CurrentEraProviderBoostTotal<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;

/// Individual history for each account that has Provider-Boosted.
#[pallet::storage]
#[pallet::getter(fn get_staking_history_for)]
pub type ProviderBoostHistories<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, ProviderBoostHistory<T>>;

/// stores how many times an account has retargeted, and when it last retargeted.
#[pallet::storage]
#[pallet::getter(fn get_retargets_for)]
pub type Retargets<T: Config> = StorageMap<_, Twox64Concat, T::AccountId, RetargetInfo<T>>;

// Simple declaration of the `Pallet` type. It is placeholder we use to implement traits and
// method.
#[pallet::pallet]
Expand Down Expand Up @@ -605,7 +619,7 @@ pub mod pallet {
let (mut boosting_details, actual_amount) =
Self::ensure_can_boost(&staker, &target, &amount)?;

let capacity = Self::increase_stake_and_issue_boost(
let capacity = Self::increase_stake_and_issue_boost_capacity(
&staker,
&mut boosting_details,
&target,
Expand Down Expand Up @@ -703,34 +717,33 @@ impl<T: Config> Pallet<T> {
Ok(capacity)
}

fn increase_stake_and_issue_boost(
fn increase_stake_and_issue_boost_capacity(
staker: &T::AccountId,
staking_details: &mut StakingDetails<T>,
target: &MessageSourceId,
amount: &BalanceOf<T>,
) -> Result<BalanceOf<T>, DispatchError> {
staking_details.deposit(*amount).ok_or(ArithmeticError::Overflow)?;
Self::set_staking_account_and_lock(staker, staking_details)?;

// get the capacity generated by a Provider Boost
let capacity = Self::capacity_generated(T::RewardsProvider::capacity_boost(*amount));

let mut target_details = Self::get_target_for(staker, target).unwrap_or_default();

target_details.deposit(*amount, capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_target_details_for(staker, *target, target_details);

let mut capacity_details = Self::get_capacity_for(target).unwrap_or_default();
capacity_details.deposit(amount, &capacity).ok_or(ArithmeticError::Overflow)?;
Self::set_capacity_for(*target, capacity_details);

let era = Self::get_current_era().era_index;
let mut reward_pool =
Self::get_reward_pool_for_era(era).ok_or(Error::<T>::EraOutOfRange)?;
reward_pool.total_staked_token = reward_pool.total_staked_token.saturating_add(*amount);

Self::set_staking_account_and_lock(staker, staking_details)?;
Self::set_target_details_for(staker, *target, target_details);
Self::set_capacity_for(*target, capacity_details);
Self::set_reward_pool(era, &reward_pool);
Self::upsert_boost_history(staker, era, *amount, true)?;

let reward_pool_total = CurrentEraProviderBoostTotal::<T>::get();
CurrentEraProviderBoostTotal::<T>::set(reward_pool_total.saturating_add(*amount));
wilwade marked this conversation as resolved.
Show resolved Hide resolved

Ok(capacity)
}

Expand Down Expand Up @@ -786,10 +799,6 @@ impl<T: Config> Pallet<T> {
CapacityLedger::<T>::insert(target, capacity_details);
}

fn set_reward_pool(era: <T>::RewardEra, new_reward_pool: &RewardPoolInfo<BalanceOf<T>>) {
ProviderBoostRewardPool::<T>::set(era, Some(new_reward_pool.clone()));
}

/// Decrease a staking account's active token and reap if it goes below the minimum.
/// Returns: actual amount unstaked, plus the staking type + StakingDetails,
/// since StakingDetails may be reaped and staking type must be used to calculate the
Expand All @@ -808,11 +817,11 @@ impl<T: Config> Pallet<T> {
let staking_type = staking_account.staking_type;
if staking_type == ProviderBoost {
let era = Self::get_current_era().era_index;
let mut reward_pool =
Self::get_reward_pool_for_era(era).ok_or(Error::<T>::EraOutOfRange)?;
reward_pool.total_staked_token = reward_pool.total_staked_token.saturating_sub(amount);
Self::set_reward_pool(era, &reward_pool.clone());
Self::upsert_boost_history(&unstaker, era, actual_unstaked_amount, false)?;
let reward_pool_total = CurrentEraProviderBoostTotal::<T>::get();
CurrentEraProviderBoostTotal::<T>::set(
reward_pool_total.saturating_sub(actual_unstaked_amount),
);
}
Ok((actual_unstaked_amount, staking_type))
}
Expand Down Expand Up @@ -971,26 +980,12 @@ impl<T: Config> Pallet<T> {
};
CurrentEraInfo::<T>::set(new_era_info); // 1w

let current_reward_pool =
Self::get_reward_pool_for_era(current_era_info.era_index).unwrap_or_default(); // 1r

let past_eras_max = T::ProviderBoostHistoryLimit::get();
let entries: u32 = ProviderBoostRewardPool::<T>::count(); // 1r

if past_eras_max.eq(&entries) {
let earliest_era =
current_era_info.era_index.saturating_sub(past_eras_max.into()).add(One::one());
ProviderBoostRewardPool::<T>::remove(earliest_era); // 1w
}

let total_reward_pool =
T::RewardsProvider::reward_pool_size(current_reward_pool.total_staked_token);
let new_reward_pool = RewardPoolInfo {
total_staked_token: current_reward_pool.total_staked_token,
total_reward_pool,
unclaimed_balance: total_reward_pool,
};
ProviderBoostRewardPool::<T>::insert(new_era_info.era_index, new_reward_pool); // 1w
// carry over the current reward pool total
let current_reward_pool_total: BalanceOf<T> = CurrentEraProviderBoostTotal::<T>::get(); // 1
Self::update_provider_boost_reward_pool(
current_era_info.era_index,
current_reward_pool_total,
);
T::WeightInfo::start_new_reward_era_if_needed()
} else {
T::DbWeight::get().reads(1)
Expand Down Expand Up @@ -1099,39 +1094,34 @@ impl<T: Config> Pallet<T> {
let staking_history =
Self::get_staking_history_for(account).ok_or(Error::<T>::NotAStakingAccount)?; // cached read from has_unclaimed_rewards

let era_info = Self::get_current_era(); // cached read, ditto
let current_era_info = Self::get_current_era(); // cached read, ditto
let max_history: u32 = T::ProviderBoostHistoryLimit::get(); // 1r
let era_length: u32 = T::EraLength::get(); // 1r length in blocks
wilwade marked this conversation as resolved.
Show resolved Hide resolved
let chunk_length: u32 = T::RewardPoolChunkLength::get();

let mut reward_era = current_era_info.era_index.saturating_sub((max_history).into());
let end_era = current_era_info.era_index.saturating_sub(One::one());

let max_history: u32 = T::ProviderBoostHistoryLimit::get() - 1; // 1r
let era_length: u32 = T::EraLength::get(); // 1r
let mut reward_era = era_info.era_index.saturating_sub((max_history).into());
let end_era = era_info.era_index.saturating_sub(One::one());
// start with how much was staked in the era before the earliest for which there are eligible rewards.
let mut previous_amount: BalanceOf<T> =
staking_history.get_amount_staked_for_era(&(reward_era.saturating_sub(1u32.into())));

while reward_era.le(&end_era) {
let staked_amount = staking_history.get_amount_staked_for_era(&reward_era);
if !staked_amount.is_zero() {
let expires_at_era = reward_era.saturating_add(max_history.into());
let reward_pool =
Self::get_reward_pool_for_era(reward_era).ok_or(Error::<T>::EraOutOfRange)?; // 1r
let expires_at_block = if expires_at_era.eq(&era_info.era_index) {
era_info.started_at + era_length.into() // expires at end of this era
} else {
let eras_to_expiration =
expires_at_era.saturating_sub(era_info.era_index).add(1u32.into());
let blocks_to_expiration = eras_to_expiration * era_length.into();
let started_at = era_info.started_at;
started_at + blocks_to_expiration.into()
};
let expires_at_block = Self::block_at_end_of_era(expires_at_era);
let eligible_amount = if staked_amount.lt(&previous_amount) {
staked_amount
} else {
previous_amount
};
let total_for_era =
Self::get_total_stake_for_past_era(reward_era, current_era_info.era_index)?;
let earned_amount = <T>::RewardsProvider::era_staking_reward(
eligible_amount,
reward_pool.total_staked_token,
reward_pool.total_reward_pool,
total_for_era,
T::RewardPoolEachEra::get(),
);
unclaimed_rewards
.try_push(UnclaimedRewardInfo {
Expand All @@ -1141,13 +1131,98 @@ impl<T: Config> Pallet<T> {
earned_amount,
})
.map_err(|_e| Error::<T>::CollectionBoundExceeded)?;
// ^^ there's no good reason for this ever to fail in production but it should be handled.
// ^^ there's no good reason for this ever to fail in production but it must be handled.
previous_amount = staked_amount;
}
reward_era = reward_era.saturating_add(One::one());
} // 1r * up to ProviderBoostHistoryLimit-1, if they staked every RewardEra.
Ok(unclaimed_rewards)
}

// Returns the block number for the end of the provided era. Assumes `era` is at least this
// era or in the future
pub(crate) fn block_at_end_of_era(era: T::RewardEra) -> BlockNumberFor<T> {
let current_era_info = Self::get_current_era();
let era_length: BlockNumberFor<T> = T::EraLength::get().into();

let era_diff = if current_era_info.era_index.eq(&era) {
wilwade marked this conversation as resolved.
Show resolved Hide resolved
1u32.into()
} else {
wilwade marked this conversation as resolved.
Show resolved Hide resolved
era.saturating_sub(current_era_info.era_index).saturating_add(1u32.into())
};
current_era_info.started_at + era_length.mul(era_diff.into()) - 1u32.into()
wilwade marked this conversation as resolved.
Show resolved Hide resolved
}

// Figure out the history chunk that a given era is in and pull out the total stake for that era.
pub(crate) fn get_total_stake_for_past_era(
reward_era: T::RewardEra,
current_era: T::RewardEra,
) -> Result<BalanceOf<T>, DispatchError> {
let chunk_idx: u32 = Self::get_chunk_index_for_era(reward_era, current_era)
.ok_or(Error::<T>::EraOutOfRange)?;
let reward_pool_chunk = Self::get_reward_pool_chunk(chunk_idx).unwrap_or_default(); // 1r
let total_for_era =
reward_pool_chunk.total_for_era(&reward_era).ok_or(Error::<T>::EraOutOfRange)?;
Ok(*total_for_era)
}

pub(crate) fn get_chunk_index_for_era(
era: T::RewardEra,
current_era: T::RewardEra,
) -> Option<u32> {
shannonwells marked this conversation as resolved.
Show resolved Hide resolved
if era >= current_era {
return None;
}

let history_limit: u32 = T::ProviderBoostHistoryLimit::get();
let era_diff = current_era.saturating_sub(era);
if era_diff > history_limit.into() {
return None;
}

let chunk_len = T::RewardPoolChunkLength::get();
let chunks: u32 = history_limit.saturating_div(chunk_len);
(0u32..chunks).find(|&i| era_diff.le(&(chunk_len * (i + 1)).into()))
shannonwells marked this conversation as resolved.
Show resolved Hide resolved
}

// This is where the reward pool gets updated.
// This inserts what was the current era and total boost amount into Reward Pool history, by
// removing the oldest item in each chunk, saving it for the next chunk, then
// inserting the new item. If the entire history is full, it exits without an insert on the last chunk,
// effectively dropping the oldest item.
shannonwells marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) fn update_provider_boost_reward_pool(era: T::RewardEra, boost_total: BalanceOf<T>) {
let mut new_era = era;
let mut new_value = boost_total;
let chunks_total =
T::ProviderBoostHistoryLimit::get().saturating_div(T::RewardPoolChunkLength::get());
for chunk_idx in 0u32..chunks_total {
// ProviderBoostRewardPools should have already been initialized with empty BoundedBTrees.
if let Some(mut chunk) = ProviderBoostRewardPools::<T>::get(chunk_idx) {
if chunk.is_full() {
// this would return None only if the history is empty, and that clearly won't happen if the chunk is full.
if let Some(oldest_era) = chunk.earliest_era() {
// this is an ummutable borrow
let mut new_chunk = chunk.clone(); // have to do it this way because E0502
if let Some(oldest_value) = new_chunk.remove(&oldest_era) {
let try_result = new_chunk.try_insert(new_era, new_value);
if try_result.is_ok() {
new_era = *oldest_era;
new_value = oldest_value;
}
ProviderBoostRewardPools::<T>::set(chunk_idx, Some(new_chunk));
}
}
} else {
// Since it's not full, just insert it and ignore the result.
// The only reason this is supposed to fail is if the BoundedBTree is full, and
// since we're here, this shouldn't ever fail.
let _unused = chunk.try_insert(new_era, new_value);
ProviderBoostRewardPools::<T>::set(chunk_idx, Some(chunk));
break;
}
}
}
}
}

/// Nontransferable functions are intended for capacity spend and recharge.
Expand Down
Loading