Skip to content

Commit

Permalink
dApp staking improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Dinonard committed Jun 26, 2024
1 parent a5bfa0c commit 5861b92
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 136 deletions.
314 changes: 180 additions & 134 deletions pallets/dapp-staking-v3/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1223,87 +1223,7 @@ pub mod pallet {
Self::ensure_pallet_enabled()?;
let account = ensure_signed(origin)?;

let mut ledger = Ledger::<T>::get(&account);
let staked_period = ledger
.staked_period()
.ok_or(Error::<T>::NoClaimableRewards)?;

// Check if the rewards have expired
let protocol_state = ActiveProtocolState::<T>::get();
ensure!(
staked_period >= Self::oldest_claimable_period(protocol_state.period_number()),
Error::<T>::RewardExpired
);

// Calculate the reward claim span
let earliest_staked_era = ledger
.earliest_staked_era()
.ok_or(Error::<T>::InternalClaimStakerError)?;
let era_rewards =
EraRewards::<T>::get(Self::era_reward_span_index(earliest_staked_era))
.ok_or(Error::<T>::NoClaimableRewards)?;

// The last era for which we can theoretically claim rewards.
// And indicator if we know the period's ending era.
let (last_period_era, period_end) = if staked_period == protocol_state.period_number() {
(protocol_state.era.saturating_sub(1), None)
} else {
PeriodEnd::<T>::get(&staked_period)
.map(|info| (info.final_era, Some(info.final_era)))
.ok_or(Error::<T>::InternalClaimStakerError)?
};

// The last era for which we can claim rewards for this account.
let last_claim_era = era_rewards.last_era().min(last_period_era);

// Get chunks for reward claiming
let rewards_iter =
ledger
.claim_up_to_era(last_claim_era, period_end)
.map_err(|err| match err {
AccountLedgerError::NothingToClaim => Error::<T>::NoClaimableRewards,
_ => Error::<T>::InternalClaimStakerError,
})?;

// Calculate rewards
let mut rewards: Vec<_> = Vec::new();
let mut reward_sum = Balance::zero();
for (era, amount) in rewards_iter {
let era_reward = era_rewards
.get(era)
.ok_or(Error::<T>::InternalClaimStakerError)?;

// Optimization, and zero-division protection
if amount.is_zero() || era_reward.staked.is_zero() {
continue;
}
let staker_reward = Perbill::from_rational(amount, era_reward.staked)
* era_reward.staker_reward_pool;

rewards.push((era, staker_reward));
reward_sum.saturating_accrue(staker_reward);
}
let rewards_len: u32 = rewards.len().unique_saturated_into();

T::StakingRewardHandler::payout_reward(&account, reward_sum)
.map_err(|_| Error::<T>::RewardPayoutFailed)?;

Self::update_ledger(&account, ledger)?;

rewards.into_iter().for_each(|(era, reward)| {
Self::deposit_event(Event::<T>::Reward {
account: account.clone(),
era,
amount: reward,
});
});

Ok(Some(if period_end.is_some() {
T::WeightInfo::claim_staker_rewards_past_period(rewards_len)
} else {
T::WeightInfo::claim_staker_rewards_ongoing_period(rewards_len)
})
.into())
Self::internal_claim_staker_rewards_for(account)
}

/// Used to claim bonus reward for a smart contract, if eligible.
Expand All @@ -1316,59 +1236,7 @@ pub mod pallet {
Self::ensure_pallet_enabled()?;
let account = ensure_signed(origin)?;

let staker_info = StakerInfo::<T>::get(&account, &smart_contract)
.ok_or(Error::<T>::NoClaimableRewards)?;
let protocol_state = ActiveProtocolState::<T>::get();

// Ensure:
// 1. Period for which rewards are being claimed has ended.
// 2. Account has been a loyal staker.
// 3. Rewards haven't expired.
let staked_period = staker_info.period_number();
ensure!(
staked_period < protocol_state.period_number(),
Error::<T>::NoClaimableRewards
);
ensure!(
staker_info.is_loyal(),
Error::<T>::NotEligibleForBonusReward
);
ensure!(
staker_info.period_number()
>= Self::oldest_claimable_period(protocol_state.period_number()),
Error::<T>::RewardExpired
);

let period_end_info =
PeriodEnd::<T>::get(&staked_period).ok_or(Error::<T>::InternalClaimBonusError)?;
// Defensive check - we should never get this far in function if no voting period stake exists.
ensure!(
!period_end_info.total_vp_stake.is_zero(),
Error::<T>::InternalClaimBonusError
);

let eligible_amount = staker_info.staked_amount(Subperiod::Voting);
let bonus_reward =
Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake)
* period_end_info.bonus_reward_pool;

T::StakingRewardHandler::payout_reward(&account, bonus_reward)
.map_err(|_| Error::<T>::RewardPayoutFailed)?;

// Cleanup entry since the reward has been claimed
StakerInfo::<T>::remove(&account, &smart_contract);
Ledger::<T>::mutate(&account, |ledger| {
ledger.contract_stake_count.saturating_dec();
});

Self::deposit_event(Event::<T>::BonusReward {
account: account.clone(),
smart_contract,
period: staked_period,
amount: bonus_reward,
});

Ok(())
Self::internal_claim_bonus_reward_for(account, smart_contract)
}

/// Used to claim dApp reward for the specified era.
Expand Down Expand Up @@ -1591,6 +1459,38 @@ pub mod pallet {
Ok(())
}

/// Claims some staker rewards for the specified account, if they have any.
/// In the case of a successful call, at least one era will be claimed, with the possibility of multiple claims happening.
#[pallet::call_index(19)]
#[pallet::weight({
let max_span_length = T::EraRewardSpanLength::get();
T::WeightInfo::claim_staker_rewards_ongoing_period(max_span_length)
.max(T::WeightInfo::claim_staker_rewards_past_period(max_span_length))
})]
pub fn claim_staker_rewards_for(
origin: OriginFor<T>,
account: T::AccountId,
) -> DispatchResultWithPostInfo {
Self::ensure_pallet_enabled()?;
ensure_signed(origin)?;

Self::internal_claim_staker_rewards_for(account)
}

/// Used to claim bonus reward for a smart contract on behalf of the specified account, if eligible.
#[pallet::call_index(20)]
#[pallet::weight(T::WeightInfo::claim_bonus_reward())]
pub fn claim_bonus_reward_for(
origin: OriginFor<T>,
account: T::AccountId,
smart_contract: T::SmartContract,
) -> DispatchResult {
Self::ensure_pallet_enabled()?;
ensure_signed(origin)?;

Self::internal_claim_bonus_reward_for(account, smart_contract)
}

/// A call used to fix accounts with inconsistent state, where frozen balance is actually higher than what's available.
///
/// The approach is as simple as possible:
Expand Down Expand Up @@ -2144,6 +2044,7 @@ pub mod pallet {
T::WeightInfo::on_idle_cleanup()
}

/// Internal function that executes teh `claim_unlocked` logic for the specified account.
fn internal_claim_unlocked(account: T::AccountId) -> DispatchResultWithPostInfo {
let mut ledger = Ledger::<T>::get(&account);

Expand All @@ -2168,5 +2069,150 @@ pub mod pallet {

Ok(Some(T::WeightInfo::claim_unlocked(removed_entries)).into())
}

/// Internal function that executes the `claim_staker_rewards_` logic for the specified account.
fn internal_claim_staker_rewards_for(account: T::AccountId) -> DispatchResultWithPostInfo {
let mut ledger = Ledger::<T>::get(&account);
let staked_period = ledger
.staked_period()
.ok_or(Error::<T>::NoClaimableRewards)?;

// Check if the rewards have expired
let protocol_state = ActiveProtocolState::<T>::get();
ensure!(
staked_period >= Self::oldest_claimable_period(protocol_state.period_number()),
Error::<T>::RewardExpired
);

// Calculate the reward claim span
let earliest_staked_era = ledger
.earliest_staked_era()
.ok_or(Error::<T>::InternalClaimStakerError)?;
let era_rewards =
EraRewards::<T>::get(Self::era_reward_span_index(earliest_staked_era))
.ok_or(Error::<T>::NoClaimableRewards)?;

// The last era for which we can theoretically claim rewards.
// And indicator if we know the period's ending era.
let (last_period_era, period_end) = if staked_period == protocol_state.period_number() {
(protocol_state.era.saturating_sub(1), None)
} else {
PeriodEnd::<T>::get(&staked_period)
.map(|info| (info.final_era, Some(info.final_era)))
.ok_or(Error::<T>::InternalClaimStakerError)?
};

// The last era for which we can claim rewards for this account.
let last_claim_era = era_rewards.last_era().min(last_period_era);

// Get chunks for reward claiming
let rewards_iter =
ledger
.claim_up_to_era(last_claim_era, period_end)
.map_err(|err| match err {
AccountLedgerError::NothingToClaim => Error::<T>::NoClaimableRewards,
_ => Error::<T>::InternalClaimStakerError,
})?;

// Calculate rewards
let mut rewards: Vec<_> = Vec::new();
let mut reward_sum = Balance::zero();
for (era, amount) in rewards_iter {
let era_reward = era_rewards
.get(era)
.ok_or(Error::<T>::InternalClaimStakerError)?;

// Optimization, and zero-division protection
if amount.is_zero() || era_reward.staked.is_zero() {
continue;
}
let staker_reward = Perbill::from_rational(amount, era_reward.staked)
* era_reward.staker_reward_pool;

rewards.push((era, staker_reward));
reward_sum.saturating_accrue(staker_reward);
}
let rewards_len: u32 = rewards.len().unique_saturated_into();

T::StakingRewardHandler::payout_reward(&account, reward_sum)
.map_err(|_| Error::<T>::RewardPayoutFailed)?;

Self::update_ledger(&account, ledger)?;

rewards.into_iter().for_each(|(era, reward)| {
Self::deposit_event(Event::<T>::Reward {
account: account.clone(),
era,
amount: reward,
});
});

Ok(Some(if period_end.is_some() {
T::WeightInfo::claim_staker_rewards_past_period(rewards_len)
} else {
T::WeightInfo::claim_staker_rewards_ongoing_period(rewards_len)
})
.into())
}

/// Internal function that executes the `claim_bonus_reward` logic for the specified account & smart contract.
fn internal_claim_bonus_reward_for(
account: T::AccountId,
smart_contract: T::SmartContract,
) -> DispatchResult {
let staker_info = StakerInfo::<T>::get(&account, &smart_contract)
.ok_or(Error::<T>::NoClaimableRewards)?;
let protocol_state = ActiveProtocolState::<T>::get();

// Ensure:
// 1. Period for which rewards are being claimed has ended.
// 2. Account has been a loyal staker.
// 3. Rewards haven't expired.
let staked_period = staker_info.period_number();
ensure!(
staked_period < protocol_state.period_number(),
Error::<T>::NoClaimableRewards
);
ensure!(
staker_info.is_loyal(),
Error::<T>::NotEligibleForBonusReward
);
ensure!(
staker_info.period_number()
>= Self::oldest_claimable_period(protocol_state.period_number()),
Error::<T>::RewardExpired
);

let period_end_info =
PeriodEnd::<T>::get(&staked_period).ok_or(Error::<T>::InternalClaimBonusError)?;
// Defensive check - we should never get this far in function if no voting period stake exists.
ensure!(
!period_end_info.total_vp_stake.is_zero(),
Error::<T>::InternalClaimBonusError
);

let eligible_amount = staker_info.staked_amount(Subperiod::Voting);
let bonus_reward =
Perbill::from_rational(eligible_amount, period_end_info.total_vp_stake)
* period_end_info.bonus_reward_pool;

T::StakingRewardHandler::payout_reward(&account, bonus_reward)
.map_err(|_| Error::<T>::RewardPayoutFailed)?;

// Cleanup entry since the reward has been claimed
StakerInfo::<T>::remove(&account, &smart_contract);
Ledger::<T>::mutate(&account, |ledger| {
ledger.contract_stake_count.saturating_dec();
});

Self::deposit_event(Event::<T>::BonusReward {
account: account.clone(),
smart_contract,
period: staked_period,
amount: bonus_reward,
});

Ok(())
}
}
}
2 changes: 1 addition & 1 deletion pallets/dapp-staking-v3/src/test/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{

use frame_support::{
construct_runtime, ord_parameter_types, parameter_types,
traits::{fungible::Mutate as FunMutate, ConstU128, ConstU32, EitherOfDiverse},
traits::{fungible::Mutate as FunMutate, ConstBool, ConstU128, ConstU32, EitherOfDiverse},
weights::Weight,
};
use sp_arithmetic::fixed_point::FixedU128;
Expand Down
Loading

0 comments on commit 5861b92

Please sign in to comment.