Skip to content

Commit

Permalink
Feat/check unclaimed rewards 1969 (#1972)
Browse files Browse the repository at this point in the history
# Goal
The goal of this PR is to implement `list_unclaimed_rewards`, and also
one that is lighter weight, `has_unclaimed_rewards`, which returns a
`bool` and which `unstake` extrinsic uses. Unstake now fails if there
are any unclaimed rewards.

Closes #1969 
Closes #1578
  • Loading branch information
shannonwells committed May 21, 2024
1 parent 5a0ebce commit aeaec44
Show file tree
Hide file tree
Showing 11 changed files with 591 additions and 95 deletions.
17 changes: 14 additions & 3 deletions pallets/capacity/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ pub fn setup_provider_stake<T: Config>(
caller: &T::AccountId,
target: &MessageSourceId,
staking_amount: BalanceOf<T>,
is_provider_boost: bool,
) {
let capacity_amount: BalanceOf<T> = Capacity::<T>::capacity_generated(staking_amount);

let mut staking_account = StakingDetails::<T>::default();
if is_provider_boost {
staking_account.staking_type = ProviderBoost;
}
let mut target_details = StakingTargetDetails::<BalanceOf<T>>::default();
let mut capacity_details =
CapacityDetails::<BalanceOf<T>, <T as Config>::EpochNumber>::default();
Expand Down Expand Up @@ -161,8 +165,15 @@ benchmarks! {
let target = 1;
let block_number = 4u32;

// Adds a boost history entry for this era only so unstake succeeds and there is an update
// to provider boost history.
let mut pbh: ProviderBoostHistory<T> = ProviderBoostHistory::new();
pbh.add_era_balance(&1u32.into(), &staking_amount);
ProviderBoostHistories::<T>::set(caller.clone(), Some(pbh));
set_era_and_reward_pool_at_block::<T>(1u32.into(), 1u32.into(), 1_000u32.into());
setup_provider_stake::<T>(&caller, &target, staking_amount);

setup_provider_stake::<T>(&caller, &target, staking_amount, true);

fill_unlock_chunks::<T>(&caller, T::MaxUnlockingChunks::get() - 1);
}: _ (RawOrigin::Signed(caller.clone()), target, unstaking_amount.into())
verify {
Expand Down Expand Up @@ -193,8 +204,8 @@ benchmarks! {
set_era_and_reward_pool_at_block::<T>(1u32.into(), 1u32.into(), 1_000u32.into());
register_provider::<T>(from_msa, "frommsa");
register_provider::<T>(to_msa, "tomsa");
setup_provider_stake::<T>(&caller, &from_msa, from_msa_amount);
setup_provider_stake::<T>(&caller, &to_msa, to_msa_amount);
setup_provider_stake::<T>(&caller, &from_msa, from_msa_amount, false);
setup_provider_stake::<T>(&caller, &to_msa, to_msa_amount, false);
let restake_amount: BalanceOf<T> = from_msa_amount.saturating_sub(10u32.into());

}: _ (RawOrigin::Signed(caller.clone(), ), from_msa, to_msa, restake_amount)
Expand Down
132 changes: 109 additions & 23 deletions pallets/capacity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ use frame_support::{

use sp_runtime::{
traits::{CheckedAdd, CheckedDiv, One, Saturating, Zero},
ArithmeticError, DispatchError, Perbill, Permill,
ArithmeticError, BoundedVec, DispatchError, Perbill, Permill,
};

pub use common_primitives::{
Expand Down Expand Up @@ -187,6 +187,7 @@ pub mod pallet {
+ MaxEncodedLen
+ EncodeLike
+ Into<BalanceOf<Self>>
+ Into<BlockNumberFor<Self>>
+ TypeInfo;

/// The number of blocks in a RewardEra
Expand Down Expand Up @@ -380,6 +381,7 @@ pub mod pallet {
/// Staker is attempting to stake an amount below the minimum amount.
StakingAmountBelowMinimum,
/// Staker is attempting to stake a zero amount. DEPRECATED
/// #[deprecated(since = "1.13.0", note = "Use StakingAmountBelowMinimum instead")]
ZeroAmountNotAllowed,
/// This AccountId does not have a staking account.
NotAStakingAccount,
Expand All @@ -403,22 +405,23 @@ pub mod pallet {
MaxEpochLengthExceeded,
/// Staker is attempting to stake an amount that leaves a token balance below the minimum amount.
BalanceTooLowtoStake,
/// There are no unstaked token amounts that have passed their thaw period.
NoThawedTokenAvailable,
/// Staker tried to change StakingType on an existing account
CannotChangeStakingType,
/// The Era specified is too far in the past or is in the future (15)
EraOutOfRange,
/// Rewards were already paid out for the specified Era range
IneligibleForPayoutInEraRange,
/// Attempted to retarget but from and to Provider MSA Ids were the same
CannotRetargetToSameProvider,
/// Rewards were already paid out this era
AlreadyClaimedRewardsThisEra,
/// There are no rewards eligible to claim. Rewards have expired, have already been
/// claimed, or boosting has never been done before the current era.
NoRewardsEligibleToClaim,
/// Caller must wait until the next RewardEra, claim rewards and then can boost again.
MustFirstClaimRewards,
/// Too many change_staking_target calls made in this RewardEra. (20)
MaxRetargetsExceeded,
/// There are no unstaked token amounts that have passed their thaw period.
NoThawedTokenAvailable,
/// Tried to exceed bounds of a some Bounded collection
CollectionBoundExceeded,
}

#[pallet::hooks]
Expand Down Expand Up @@ -505,6 +508,8 @@ pub mod pallet {

ensure!(requested_amount > Zero::zero(), Error::<T>::UnstakedAmountIsZero);

ensure!(!Self::has_unclaimed_rewards(&unstaker), Error::<T>::MustFirstClaimRewards);

let (actual_amount, staking_type) =
Self::decrease_active_staking_balance(&unstaker, requested_amount)?;
Self::add_unlock_chunk(&unstaker, actual_amount)?;
Expand Down Expand Up @@ -798,14 +803,17 @@ impl<T: Config> Pallet<T> {
ensure!(amount <= staking_account.active, Error::<T>::InsufficientStakingBalance);

let actual_unstaked_amount = staking_account.withdraw(amount)?;
let staking_type = staking_account.staking_type;
Self::set_staking_account(unstaker, &staking_account);

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());
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)?;
}
Ok((actual_unstaked_amount, staking_type))
}

Expand Down Expand Up @@ -914,14 +922,6 @@ impl<T: Config> Pallet<T> {

capacity_details.withdraw(actual_capacity, actual_amount);

if staking_type.eq(&ProviderBoost) {
// update staking history
let era = Self::get_current_era().era_index;
let mut pbh =
Self::get_staking_history_for(unstaker).ok_or(Error::<T>::NotAStakingAccount)?;
pbh.subtract_era_balance(&era, &actual_amount);
ProviderBoostHistories::set(unstaker, Some(pbh));
}
Self::set_capacity_for(target, capacity_details);
Self::set_target_details_for(unstaker, target, staking_target_details);

Expand Down Expand Up @@ -1058,10 +1058,96 @@ impl<T: Config> Pallet<T> {
} else {
boost_history.subtract_era_balance(&current_era, &boost_amount)
};
ensure!(upsert_result.is_some(), Error::<T>::EraOutOfRange);
ProviderBoostHistories::<T>::set(account, Some(boost_history));
match upsert_result {
Some(0usize) => ProviderBoostHistories::<T>::remove(account),
None => return Err(DispatchError::from(Error::<T>::EraOutOfRange)),

Check warning on line 1063 in pallets/capacity/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

pallets/capacity/src/lib.rs#L1063

Added line #L1063 was not covered by tests
_ => ProviderBoostHistories::<T>::set(account, Some(boost_history)),
}
Ok(())
}

pub(crate) fn has_unclaimed_rewards(account: &T::AccountId) -> bool {
let current_era = Self::get_current_era().era_index;
match Self::get_staking_history_for(account) {
Some(provider_boost_history) => {
match provider_boost_history.count() {
0usize => false,
// they staked before the current era, so they have unclaimed rewards.
1usize => provider_boost_history.get_entry_for_era(&current_era).is_none(),
_ => true,
}
},
None => false,
} // 1r
}

// this could be up to 35 reads.
#[allow(unused)]
pub(crate) fn list_unclaimed_rewards(
account: &T::AccountId,
) -> Result<BoundedVec<UnclaimedRewardInfo<T>, T::StakingRewardsPastErasMax>, DispatchError> {
let mut unclaimed_rewards: BoundedVec<
UnclaimedRewardInfo<T>,
T::StakingRewardsPastErasMax,
> = BoundedVec::new();

if !Self::has_unclaimed_rewards(account) {
// 2r
return Ok(unclaimed_rewards);
}

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 max_history: u32 = T::StakingRewardsPastErasMax::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 eligible_amount = if staked_amount.lt(&previous_amount) {
staked_amount

Check warning on line 1127 in pallets/capacity/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

pallets/capacity/src/lib.rs#L1127

Added line #L1127 was not covered by tests
} else {
previous_amount
};
let earned_amount = <T>::RewardsProvider::era_staking_reward(
eligible_amount,
reward_pool.total_staked_token,
reward_pool.total_reward_pool,
);
unclaimed_rewards
.try_push(UnclaimedRewardInfo {
reward_era,
expires_at_block,
eligible_amount,
earned_amount,
})
.map_err(|_e| Error::<T>::CollectionBoundExceeded)?;

Check warning on line 1143 in pallets/capacity/src/lib.rs

View check run for this annotation

Codecov / codecov/patch

pallets/capacity/src/lib.rs#L1143

Added line #L1143 was not covered by tests
// ^^ there's no good reason for this ever to fail in production but it should be handled.
previous_amount = staked_amount;
}
reward_era = reward_era.saturating_add(One::one());
} // 1r * up to StakingRewardsPastErasMax-1, if they staked every RewardEra.
Ok(unclaimed_rewards)
}
}

/// Nontransferable functions are intended for capacity spend and recharge.
Expand Down
6 changes: 3 additions & 3 deletions pallets/capacity/src/tests/eras_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use sp_core::Get;
fn start_new_era_if_needed_updates_era_info() {
new_test_ext().execute_with(|| {
system_run_to_block(9);
for i in 1..4 {
let block_decade = i * 10;
for i in 1..=4 {
let block_decade = (i * 10) + 1;
run_to_block(block_decade);

let current_era_info = CurrentEraInfo::<Test>::get();
Expand Down Expand Up @@ -45,7 +45,7 @@ fn start_new_era_if_needed_updates_reward_pool() {

for i in 1u32..4 {
let era = i + 1;
let final_block = i * 10;
let final_block = (i * 10) + 1;
system_run_to_block(final_block - 1);
run_to_block(final_block);
assert_eq!(StakingRewardPool::<Test>::count(), era);
Expand Down
6 changes: 3 additions & 3 deletions pallets/capacity/src/tests/mock.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use crate as pallet_capacity;

use crate::{
tests::testing_utils::set_era_and_reward_pool_at_block, BalanceOf, StakingRewardClaim,
tests::testing_utils::set_era_and_reward_pool, BalanceOf, StakingRewardClaim,
StakingRewardsProvider,
};
use common_primitives::{
Expand Down Expand Up @@ -208,7 +208,7 @@ impl pallet_capacity::Config for Test {
type CapacityPerToken = TestCapacityPerToken;
type RewardEra = TestRewardEra;
type EraLength = ConstU32<10>;
type StakingRewardsPastErasMax = ConstU32<5>;
type StakingRewardsPastErasMax = ConstU32<6>; // 5 for claiming rewards, 1 for current reward era
type RewardsProvider = Capacity;
type MaxRetargetsPerRewardEra = ConstU32<5>;
type RewardPoolEachEra = ConstU64<10_000>;
Expand All @@ -235,7 +235,7 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
let mut ext = sp_io::TestExternalities::new(t);
ext.execute_with(|| {
System::set_block_number(1);
set_era_and_reward_pool_at_block(1, 0, 0);
set_era_and_reward_pool(1, 1, 0);
});
ext
}
31 changes: 20 additions & 11 deletions pallets/capacity/src/tests/provider_boost_history_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ fn multiple_provider_boosts_updates_history_correctly() {
// should update era 1 history
let mut history = Capacity::get_staking_history_for(staker).unwrap();
assert_eq!(history.count(), 1);
assert_eq!(history.get_staking_amount_for_era(&1u32).unwrap(), &700u64);
assert_eq!(history.get_entry_for_era(&1u32).unwrap(), &700u64);

system_run_to_block(9);
run_to_block(10);
system_run_to_block(10);
run_to_block(11);

assert_ok!(Capacity::provider_boost(RuntimeOrigin::signed(staker), target, 200));

// should add an era 2 history
history = Capacity::get_staking_history_for(staker).unwrap();
assert_eq!(history.count(), 2);
assert_eq!(history.get_staking_amount_for_era(&2u32).unwrap(), &900u64);
assert_eq!(history.get_entry_for_era(&2u32).unwrap(), &900u64);
})
}

Expand Down Expand Up @@ -74,10 +74,10 @@ fn provider_boost_history_add_era_balance_adds_entries_and_deletes_old_if_full()
let new_era: <Test as Config>::RewardEra = 99;
pbh.add_era_balance(&new_era, &1000u64);
assert_eq!(pbh.count(), bound as usize);
assert!(pbh.get_staking_amount_for_era(&new_era).is_some());
assert!(pbh.get_entry_for_era(&new_era).is_some());

let first_era: <Test as Config>::RewardEra = 1;
assert!(pbh.get_staking_amount_for_era(&first_era).is_none());
assert!(pbh.get_entry_for_era(&first_era).is_none());
}

#[test]
Expand All @@ -87,7 +87,7 @@ fn provider_boost_history_add_era_balance_in_same_era_updates_entry() {
let era = 2u32;
pbh.add_era_balance(&era, &add_amount);
pbh.add_era_balance(&era, &add_amount);
assert_eq!(pbh.get_staking_amount_for_era(&era).unwrap(), &2_000u64)
assert_eq!(pbh.get_entry_for_era(&era).unwrap(), &2_000u64)
}

#[test]
Expand All @@ -96,7 +96,7 @@ fn provider_boost_history_subtract_era_balance_in_same_era_updates_entry() {
let era = 2u32;
pbh.add_era_balance(&era, &1000u64);
pbh.subtract_era_balance(&era, &300u64);
assert_eq!(pbh.get_staking_amount_for_era(&(era)).unwrap(), &700u64);
assert_eq!(pbh.get_entry_for_era(&(era)).unwrap(), &700u64);
}

#[test]
Expand All @@ -106,7 +106,7 @@ fn provider_boost_history_add_era_balance_in_new_era_correctly_adds_value() {
let era = 2u32;
pbh.add_era_balance(&era, &add_amount);
pbh.add_era_balance(&(era + 1), &add_amount);
assert_eq!(pbh.get_staking_amount_for_era(&(era + 1)).unwrap(), &2_000u64)
assert_eq!(pbh.get_entry_for_era(&(era + 1)).unwrap(), &2_000u64)
}

#[test]
Expand All @@ -116,8 +116,17 @@ fn provider_boost_history_subtract_era_balance_in_new_era_correctly_subtracts_va
pbh.add_era_balance(&era, &1000u64);

pbh.subtract_era_balance(&(era + 1), &400u64);
assert_eq!(pbh.get_staking_amount_for_era(&(era + 1)).unwrap(), &600u64);
assert_eq!(pbh.get_entry_for_era(&(era + 1)).unwrap(), &600u64);

pbh.subtract_era_balance(&(era + 2), &600u64);
assert!(pbh.get_staking_amount_for_era(&(era + 2)).unwrap().is_zero());
assert!(pbh.get_entry_for_era(&(era + 2)).unwrap().is_zero());
}

#[test]
fn provider_boost_history_subtract_all_balance_on_only_entry_returns_some_0() {
let mut pbh = ProviderBoostHistory::<Test>::new();
let era = 22u32;
let amount = 1000u64;
pbh.add_era_balance(&era, &amount);
assert_eq!(pbh.subtract_era_balance(&(era), &amount), Some(0usize));
}
Loading

0 comments on commit aeaec44

Please sign in to comment.