#![cfg_attr(not(feature = "std"), no_std)]
use codec::Codec;
use frame_support::{
defensive, defensive_assert, ensure,
pallet_prelude::{MaxEncodedLen, *},
storage::bounded_btree_map::BoundedBTreeMap,
traits::{
fungible::{
Inspect as FunInspect, InspectFreeze, Mutate as FunMutate,
MutateFreeze as FunMutateFreeze,
},
tokens::{Fortitude, Preservation},
Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating, Get,
},
DefaultNoBound, PalletError,
};
use frame_system::pallet_prelude::BlockNumberFor;
use scale_info::TypeInfo;
use sp_core::U256;
use sp_runtime::{
traits::{
AccountIdConversion, Bounded, CheckedAdd, CheckedSub, Convert, Saturating, StaticLookup,
Zero,
},
FixedPointNumber, Perbill,
};
use sp_staking::{EraIndex, StakingInterface};
use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, ops::Div, vec::Vec};
#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))]
use sp_runtime::TryRuntimeError;
pub const LOG_TARGET: &str = "runtime::nomination-pools";
#[macro_export]
macro_rules! log {
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
log::$level!(
target: $crate::LOG_TARGET,
concat!("[{:?}] 🏊♂️ ", $patter), <frame_system::Pallet<T>>::block_number() $(, $values)*
)
};
}
#[cfg(any(test, feature = "fuzzing"))]
pub mod mock;
#[cfg(test)]
mod tests;
pub mod migration;
pub mod weights;
pub use pallet::*;
pub use weights::WeightInfo;
pub type BalanceOf<T> =
<<T as Config>::Currency as FunInspect<<T as frame_system::Config>::AccountId>>::Balance;
pub type PoolId = u32;
type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
pub const POINTS_TO_BALANCE_INIT_RATIO: u32 = 1;
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, PartialEq, Clone)]
pub enum ConfigOp<T: Codec + Debug> {
Noop,
Set(T),
Remove,
}
enum BondType {
Create,
Later,
}
#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
pub enum BondExtra<Balance> {
FreeBalance(Balance),
Rewards,
}
#[derive(Encode, Decode)]
enum AccountType {
Bonded,
Reward,
}
#[derive(Encode, Decode, MaxEncodedLen, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)]
pub enum ClaimPermission {
Permissioned,
PermissionlessCompound,
PermissionlessWithdraw,
PermissionlessAll,
}
impl ClaimPermission {
fn can_bond_extra(&self) -> bool {
matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessCompound)
}
fn can_claim_payout(&self) -> bool {
matches!(self, ClaimPermission::PermissionlessAll | ClaimPermission::PermissionlessWithdraw)
}
}
impl Default for ClaimPermission {
fn default() -> Self {
Self::Permissioned
}
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound, CloneNoBound)]
#[cfg_attr(feature = "std", derive(frame_support::PartialEqNoBound, DefaultNoBound))]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct PoolMember<T: Config> {
pub pool_id: PoolId,
pub points: BalanceOf<T>,
pub last_recorded_reward_counter: T::RewardCounter,
pub unbonding_eras: BoundedBTreeMap<EraIndex, BalanceOf<T>, T::MaxUnbonding>,
}
impl<T: Config> PoolMember<T> {
fn pending_rewards(
&self,
current_reward_counter: T::RewardCounter,
) -> Result<BalanceOf<T>, Error<T>> {
(current_reward_counter.defensive_saturating_sub(self.last_recorded_reward_counter))
.checked_mul_int(self.active_points())
.ok_or(Error::<T>::OverflowRisk)
}
fn active_balance(&self) -> BalanceOf<T> {
if let Some(pool) = BondedPool::<T>::get(self.pool_id).defensive() {
pool.points_to_balance(self.points)
} else {
Zero::zero()
}
}
#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))]
fn total_balance(&self) -> BalanceOf<T> {
let pool = BondedPool::<T>::get(self.pool_id).unwrap();
let active_balance = pool.points_to_balance(self.active_points());
let sub_pools = match SubPoolsStorage::<T>::get(self.pool_id) {
Some(sub_pools) => sub_pools,
None => return active_balance,
};
let unbonding_balance = self.unbonding_eras.iter().fold(
BalanceOf::<T>::zero(),
|accumulator, (era, unlocked_points)| {
let era_pool = sub_pools.with_era.get(era).unwrap_or(&sub_pools.no_era);
accumulator + (era_pool.point_to_balance(*unlocked_points))
},
);
active_balance + unbonding_balance
}
fn total_points(&self) -> BalanceOf<T> {
self.active_points().saturating_add(self.unbonding_points())
}
fn active_points(&self) -> BalanceOf<T> {
self.points
}
fn unbonding_points(&self) -> BalanceOf<T> {
self.unbonding_eras
.as_ref()
.iter()
.fold(BalanceOf::<T>::zero(), |acc, (_, v)| acc.saturating_add(*v))
}
fn try_unbond(
&mut self,
points_dissolved: BalanceOf<T>,
points_issued: BalanceOf<T>,
unbonding_era: EraIndex,
) -> Result<(), Error<T>> {
if let Some(new_points) = self.points.checked_sub(&points_dissolved) {
match self.unbonding_eras.get_mut(&unbonding_era) {
Some(already_unbonding_points) =>
*already_unbonding_points =
already_unbonding_points.saturating_add(points_issued),
None => self
.unbonding_eras
.try_insert(unbonding_era, points_issued)
.map(|old| {
if old.is_some() {
defensive!("value checked to not exist in the map; qed");
}
})
.map_err(|_| Error::<T>::MaxUnbondingLimit)?,
}
self.points = new_points;
Ok(())
} else {
Err(Error::<T>::MinimumBondNotMet)
}
}
fn withdraw_unlocked(
&mut self,
current_era: EraIndex,
) -> BoundedBTreeMap<EraIndex, BalanceOf<T>, T::MaxUnbonding> {
let mut removed_points =
BoundedBTreeMap::<EraIndex, BalanceOf<T>, T::MaxUnbonding>::default();
self.unbonding_eras.retain(|e, p| {
if *e > current_era {
true
} else {
removed_points
.try_insert(*e, *p)
.expect("source map is bounded, this is a subset, will be bounded; qed");
false
}
});
removed_points
}
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, RuntimeDebugNoBound, Clone, Copy)]
pub enum PoolState {
Open,
Blocked,
Destroying,
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Clone)]
pub struct PoolRoles<AccountId> {
pub depositor: AccountId,
pub root: Option<AccountId>,
pub nominator: Option<AccountId>,
pub bouncer: Option<AccountId>,
}
#[derive(PartialEq, Eq, Copy, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum CommissionClaimPermission<AccountId> {
Permissionless,
Account(AccountId),
}
#[derive(
Encode, Decode, DefaultNoBound, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Copy, Clone,
)]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct Commission<T: Config> {
pub current: Option<(Perbill, T::AccountId)>,
pub max: Option<Perbill>,
pub change_rate: Option<CommissionChangeRate<BlockNumberFor<T>>>,
pub throttle_from: Option<BlockNumberFor<T>>,
pub claim_permission: Option<CommissionClaimPermission<T::AccountId>>,
}
impl<T: Config> Commission<T> {
fn throttling(&self, to: &Perbill) -> bool {
if let Some(t) = self.change_rate.as_ref() {
let commission_as_percent =
self.current.as_ref().map(|(x, _)| *x).unwrap_or(Perbill::zero());
if *to <= commission_as_percent {
return false
}
if (*to).saturating_sub(commission_as_percent) > t.max_increase {
return true
}
return self.throttle_from.map_or_else(
|| {
defensive!("throttle_from should exist if change_rate is set");
true
},
|f| {
if t.min_delay == Zero::zero() {
false
} else {
let blocks_surpassed =
<frame_system::Pallet<T>>::block_number().saturating_sub(f);
blocks_surpassed < t.min_delay
}
},
)
}
false
}
fn current(&self) -> Perbill {
self.current
.as_ref()
.map_or(Perbill::zero(), |(c, _)| *c)
.min(GlobalMaxCommission::<T>::get().unwrap_or(Bounded::max_value()))
}
fn try_update_current(&mut self, current: &Option<(Perbill, T::AccountId)>) -> DispatchResult {
self.current = match current {
None => None,
Some((commission, payee)) => {
ensure!(!self.throttling(commission), Error::<T>::CommissionChangeThrottled);
ensure!(
commission <= &GlobalMaxCommission::<T>::get().unwrap_or(Bounded::max_value()),
Error::<T>::CommissionExceedsGlobalMaximum
);
ensure!(
self.max.map_or(true, |m| commission <= &m),
Error::<T>::CommissionExceedsMaximum
);
if commission.is_zero() {
None
} else {
Some((*commission, payee.clone()))
}
},
};
self.register_update();
Ok(())
}
fn try_update_max(&mut self, pool_id: PoolId, new_max: Perbill) -> DispatchResult {
ensure!(
new_max <= GlobalMaxCommission::<T>::get().unwrap_or(Bounded::max_value()),
Error::<T>::CommissionExceedsGlobalMaximum
);
if let Some(old) = self.max.as_mut() {
if new_max > *old {
return Err(Error::<T>::MaxCommissionRestricted.into())
}
*old = new_max;
} else {
self.max = Some(new_max)
};
let updated_current = self
.current
.as_mut()
.map(|(c, _)| {
let u = *c > new_max;
*c = (*c).min(new_max);
u
})
.unwrap_or(false);
if updated_current {
if let Some((_, payee)) = self.current.as_ref() {
Pallet::<T>::deposit_event(Event::<T>::PoolCommissionUpdated {
pool_id,
current: Some((new_max, payee.clone())),
});
}
self.register_update();
}
Ok(())
}
fn try_update_change_rate(
&mut self,
change_rate: CommissionChangeRate<BlockNumberFor<T>>,
) -> DispatchResult {
ensure!(!&self.less_restrictive(&change_rate), Error::<T>::CommissionChangeRateNotAllowed);
if self.change_rate.is_none() {
self.register_update();
}
self.change_rate = Some(change_rate);
Ok(())
}
fn register_update(&mut self) {
self.throttle_from = Some(<frame_system::Pallet<T>>::block_number());
}
fn less_restrictive(&self, new: &CommissionChangeRate<BlockNumberFor<T>>) -> bool {
self.change_rate
.as_ref()
.map(|c| new.max_increase > c.max_increase || new.min_delay < c.min_delay)
.unwrap_or(false)
}
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Debug, PartialEq, Copy, Clone)]
pub struct CommissionChangeRate<BlockNumber> {
pub max_increase: Perbill,
pub min_delay: BlockNumber,
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Clone)]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct BondedPoolInner<T: Config> {
pub commission: Commission<T>,
pub member_counter: u32,
pub points: BalanceOf<T>,
pub roles: PoolRoles<T::AccountId>,
pub state: PoolState,
}
#[derive(RuntimeDebugNoBound)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq))]
pub struct BondedPool<T: Config> {
id: PoolId,
inner: BondedPoolInner<T>,
}
impl<T: Config> sp_std::ops::Deref for BondedPool<T> {
type Target = BondedPoolInner<T>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T: Config> sp_std::ops::DerefMut for BondedPool<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}
impl<T: Config> BondedPool<T> {
fn new(id: PoolId, roles: PoolRoles<T::AccountId>) -> Self {
Self {
id,
inner: BondedPoolInner {
commission: Commission::default(),
member_counter: Zero::zero(),
points: Zero::zero(),
roles,
state: PoolState::Open,
},
}
}
pub fn get(id: PoolId) -> Option<Self> {
BondedPools::<T>::try_get(id).ok().map(|inner| Self { id, inner })
}
fn bonded_account(&self) -> T::AccountId {
Pallet::<T>::create_bonded_account(self.id)
}
fn reward_account(&self) -> T::AccountId {
Pallet::<T>::create_reward_account(self.id)
}
fn put(self) {
BondedPools::<T>::insert(self.id, self.inner);
}
fn remove(self) {
BondedPools::<T>::remove(self.id);
}
fn balance_to_point(&self, new_funds: BalanceOf<T>) -> BalanceOf<T> {
let bonded_balance =
T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero());
Pallet::<T>::balance_to_point(bonded_balance, self.points, new_funds)
}
fn points_to_balance(&self, points: BalanceOf<T>) -> BalanceOf<T> {
let bonded_balance =
T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero());
Pallet::<T>::point_to_balance(bonded_balance, self.points, points)
}
fn issue(&mut self, new_funds: BalanceOf<T>) -> BalanceOf<T> {
let points_to_issue = self.balance_to_point(new_funds);
self.points = self.points.saturating_add(points_to_issue);
points_to_issue
}
fn dissolve(&mut self, points: BalanceOf<T>) -> BalanceOf<T> {
let balance = self.points_to_balance(points);
self.points = self.points.saturating_sub(points);
balance
}
fn try_inc_members(&mut self) -> Result<(), DispatchError> {
ensure!(
MaxPoolMembersPerPool::<T>::get()
.map_or(true, |max_per_pool| self.member_counter < max_per_pool),
Error::<T>::MaxPoolMembers
);
ensure!(
MaxPoolMembers::<T>::get().map_or(true, |max| PoolMembers::<T>::count() < max),
Error::<T>::MaxPoolMembers
);
self.member_counter = self.member_counter.checked_add(1).ok_or(Error::<T>::OverflowRisk)?;
Ok(())
}
fn dec_members(mut self) -> Self {
self.member_counter = self.member_counter.defensive_saturating_sub(1);
self
}
fn transferable_balance(&self) -> BalanceOf<T> {
let account = self.bonded_account();
T::Currency::balance(&account)
.saturating_sub(T::Staking::active_stake(&account).unwrap_or_default())
}
fn is_root(&self, who: &T::AccountId) -> bool {
self.roles.root.as_ref().map_or(false, |root| root == who)
}
fn is_bouncer(&self, who: &T::AccountId) -> bool {
self.roles.bouncer.as_ref().map_or(false, |bouncer| bouncer == who)
}
fn can_update_roles(&self, who: &T::AccountId) -> bool {
self.is_root(who)
}
fn can_nominate(&self, who: &T::AccountId) -> bool {
self.is_root(who) ||
self.roles.nominator.as_ref().map_or(false, |nominator| nominator == who)
}
fn can_kick(&self, who: &T::AccountId) -> bool {
self.state == PoolState::Blocked && (self.is_root(who) || self.is_bouncer(who))
}
fn can_toggle_state(&self, who: &T::AccountId) -> bool {
(self.is_root(who) || self.is_bouncer(who)) && !self.is_destroying()
}
fn can_set_metadata(&self, who: &T::AccountId) -> bool {
self.is_root(who) || self.is_bouncer(who)
}
fn can_manage_commission(&self, who: &T::AccountId) -> bool {
self.is_root(who)
}
fn can_claim_commission(&self, who: &T::AccountId) -> bool {
if let Some(permission) = self.commission.claim_permission.as_ref() {
match permission {
CommissionClaimPermission::Permissionless => true,
CommissionClaimPermission::Account(account) => account == who || self.is_root(who),
}
} else {
self.is_root(who)
}
}
fn is_destroying(&self) -> bool {
matches!(self.state, PoolState::Destroying)
}
fn is_destroying_and_only_depositor(&self, alleged_depositor_points: BalanceOf<T>) -> bool {
self.is_destroying() && self.points == alleged_depositor_points && self.member_counter == 1
}
fn ok_to_be_open(&self) -> Result<(), DispatchError> {
ensure!(!self.is_destroying(), Error::<T>::CanNotChangeState);
let bonded_balance =
T::Staking::active_stake(&self.bonded_account()).unwrap_or(Zero::zero());
ensure!(!bonded_balance.is_zero(), Error::<T>::OverflowRisk);
let points_to_balance_ratio_floor = self
.points
.div(bonded_balance);
let max_points_to_balance = T::MaxPointsToBalance::get();
ensure!(
points_to_balance_ratio_floor < max_points_to_balance.into(),
Error::<T>::OverflowRisk
);
Ok(())
}
fn ok_to_join(&self) -> Result<(), DispatchError> {
ensure!(self.state == PoolState::Open, Error::<T>::NotOpen);
self.ok_to_be_open()?;
Ok(())
}
fn ok_to_unbond_with(
&self,
caller: &T::AccountId,
target_account: &T::AccountId,
target_member: &PoolMember<T>,
unbonding_points: BalanceOf<T>,
) -> Result<(), DispatchError> {
let is_permissioned = caller == target_account;
let is_depositor = *target_account == self.roles.depositor;
let is_full_unbond = unbonding_points == target_member.active_points();
let balance_after_unbond = {
let new_depositor_points =
target_member.active_points().saturating_sub(unbonding_points);
let mut target_member_after_unbond = (*target_member).clone();
target_member_after_unbond.points = new_depositor_points;
target_member_after_unbond.active_balance()
};
ensure!(
is_permissioned || is_full_unbond,
Error::<T>::PartialUnbondNotAllowedPermissionlessly
);
ensure!(
is_full_unbond ||
balance_after_unbond >=
if is_depositor {
Pallet::<T>::depositor_min_bond()
} else {
MinJoinBond::<T>::get()
},
Error::<T>::MinimumBondNotMet
);
match (is_permissioned, is_depositor) {
(true, false) => (),
(true, true) => {
if self.is_destroying_and_only_depositor(target_member.active_points()) {
} else {
ensure!(!is_full_unbond, Error::<T>::MinimumBondNotMet);
}
},
(false, false) => {
debug_assert!(is_full_unbond);
ensure!(
self.can_kick(caller) || self.is_destroying(),
Error::<T>::NotKickerOrDestroying
)
},
(false, true) => {
return Err(Error::<T>::DoesNotHavePermission.into())
},
};
Ok(())
}
fn ok_to_withdraw_unbonded_with(
&self,
caller: &T::AccountId,
target_account: &T::AccountId,
) -> Result<(), DispatchError> {
let is_permissioned = caller == target_account;
ensure!(
is_permissioned || self.can_kick(caller) || self.is_destroying(),
Error::<T>::NotKickerOrDestroying
);
Ok(())
}
fn try_bond_funds(
&mut self,
who: &T::AccountId,
amount: BalanceOf<T>,
ty: BondType,
) -> Result<BalanceOf<T>, DispatchError> {
let bonded_account = self.bonded_account();
T::Currency::transfer(
who,
&bonded_account,
amount,
match ty {
BondType::Create => Preservation::Expendable,
BondType::Later => Preservation::Preserve,
},
)?;
let points_issued = self.issue(amount);
match ty {
BondType::Create => T::Staking::bond(&bonded_account, amount, &self.reward_account())?,
BondType::Later => T::Staking::bond_extra(&bonded_account, amount)?,
}
TotalValueLocked::<T>::mutate(|tvl| {
tvl.saturating_accrue(amount);
});
Ok(points_issued)
}
fn set_state(&mut self, state: PoolState) {
if self.state != state {
self.state = state;
Pallet::<T>::deposit_event(Event::<T>::StateChanged {
pool_id: self.id,
new_state: state,
});
};
}
fn withdraw_from_staking(&self, num_slashing_spans: u32) -> Result<bool, DispatchError> {
let bonded_account = self.bonded_account();
let prev_total = T::Staking::total_stake(&bonded_account.clone()).unwrap_or_default();
let outcome = T::Staking::withdraw_unbonded(bonded_account.clone(), num_slashing_spans);
let diff = prev_total
.defensive_saturating_sub(T::Staking::total_stake(&bonded_account).unwrap_or_default());
TotalValueLocked::<T>::mutate(|tvl| {
tvl.saturating_reduce(diff);
});
outcome
}
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, RuntimeDebugNoBound)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq, DefaultNoBound))]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct RewardPool<T: Config> {
last_recorded_reward_counter: T::RewardCounter,
last_recorded_total_payouts: BalanceOf<T>,
total_rewards_claimed: BalanceOf<T>,
total_commission_pending: BalanceOf<T>,
total_commission_claimed: BalanceOf<T>,
}
impl<T: Config> RewardPool<T> {
pub(crate) fn last_recorded_reward_counter(&self) -> T::RewardCounter {
self.last_recorded_reward_counter
}
fn register_claimed_reward(&mut self, reward: BalanceOf<T>) {
self.total_rewards_claimed = self.total_rewards_claimed.saturating_add(reward);
}
fn update_records(
&mut self,
id: PoolId,
bonded_points: BalanceOf<T>,
commission: Perbill,
) -> Result<(), Error<T>> {
let balance = Self::current_balance(id);
let (current_reward_counter, new_pending_commission) =
self.current_reward_counter(id, bonded_points, commission)?;
self.last_recorded_reward_counter = current_reward_counter;
self.total_commission_pending =
self.total_commission_pending.saturating_add(new_pending_commission);
let last_recorded_total_payouts = balance
.checked_add(&self.total_rewards_claimed.saturating_add(self.total_commission_claimed))
.ok_or(Error::<T>::OverflowRisk)?;
self.last_recorded_total_payouts =
self.last_recorded_total_payouts.max(last_recorded_total_payouts);
Ok(())
}
fn current_reward_counter(
&self,
id: PoolId,
bonded_points: BalanceOf<T>,
commission: Perbill,
) -> Result<(T::RewardCounter, BalanceOf<T>), Error<T>> {
let balance = Self::current_balance(id);
let current_payout_balance = balance
.saturating_add(self.total_rewards_claimed)
.saturating_add(self.total_commission_claimed)
.saturating_sub(self.last_recorded_total_payouts);
let new_pending_commission = commission * current_payout_balance;
let new_pending_rewards = current_payout_balance.saturating_sub(new_pending_commission);
let current_reward_counter =
T::RewardCounter::checked_from_rational(new_pending_rewards, bonded_points)
.and_then(|ref r| self.last_recorded_reward_counter.checked_add(r))
.ok_or(Error::<T>::OverflowRisk)?;
Ok((current_reward_counter, new_pending_commission))
}
fn current_balance(id: PoolId) -> BalanceOf<T> {
T::Currency::reducible_balance(
&Pallet::<T>::create_reward_account(id),
Preservation::Expendable,
Fortitude::Polite,
)
}
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DefaultNoBound, RuntimeDebugNoBound)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq, Eq))]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct UnbondPool<T: Config> {
points: BalanceOf<T>,
balance: BalanceOf<T>,
}
impl<T: Config> UnbondPool<T> {
fn balance_to_point(&self, new_funds: BalanceOf<T>) -> BalanceOf<T> {
Pallet::<T>::balance_to_point(self.balance, self.points, new_funds)
}
fn point_to_balance(&self, points: BalanceOf<T>) -> BalanceOf<T> {
Pallet::<T>::point_to_balance(self.balance, self.points, points)
}
fn issue(&mut self, new_funds: BalanceOf<T>) -> BalanceOf<T> {
let new_points = self.balance_to_point(new_funds);
self.points = self.points.saturating_add(new_points);
self.balance = self.balance.saturating_add(new_funds);
new_points
}
fn dissolve(&mut self, points: BalanceOf<T>) -> BalanceOf<T> {
let balance_to_unbond = self.point_to_balance(points);
self.points = self.points.saturating_sub(points);
self.balance = self.balance.saturating_sub(balance_to_unbond);
balance_to_unbond
}
}
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DefaultNoBound, RuntimeDebugNoBound)]
#[cfg_attr(feature = "std", derive(Clone, PartialEq))]
#[codec(mel_bound(T: Config))]
#[scale_info(skip_type_params(T))]
pub struct SubPools<T: Config> {
no_era: UnbondPool<T>,
with_era: BoundedBTreeMap<EraIndex, UnbondPool<T>, TotalUnbondingPools<T>>,
}
impl<T: Config> SubPools<T> {
fn maybe_merge_pools(mut self, current_era: EraIndex) -> Self {
if let Some(newest_era_to_remove) =
current_era.checked_sub(T::PostUnbondingPoolsWindow::get())
{
self.with_era.retain(|k, v| {
if *k > newest_era_to_remove {
true
} else {
self.no_era.points = self.no_era.points.saturating_add(v.points);
self.no_era.balance = self.no_era.balance.saturating_add(v.balance);
false
}
});
}
self
}
#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))]
fn sum_unbonding_balance(&self) -> BalanceOf<T> {
self.no_era.balance.saturating_add(
self.with_era
.values()
.fold(BalanceOf::<T>::zero(), |acc, pool| acc.saturating_add(pool.balance)),
)
}
}
pub struct TotalUnbondingPools<T: Config>(PhantomData<T>);
impl<T: Config> Get<u32> for TotalUnbondingPools<T> {
fn get() -> u32 {
T::Staking::bonding_duration() + T::PostUnbondingPoolsWindow::get()
}
}
#[frame_support::pallet]
pub mod pallet {
use super::*;
use frame_support::traits::StorageVersion;
use frame_system::{ensure_signed, pallet_prelude::*};
use sp_runtime::Perbill;
const STORAGE_VERSION: StorageVersion = StorageVersion::new(8);
#[pallet::pallet]
#[pallet::storage_version(STORAGE_VERSION)]
pub struct Pallet<T>(_);
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type WeightInfo: weights::WeightInfo;
type Currency: FunMutate<Self::AccountId>
+ FunMutateFreeze<Self::AccountId, Id = Self::RuntimeFreezeReason>;
type RuntimeFreezeReason: From<FreezeReason>;
type RewardCounter: FixedPointNumber + MaxEncodedLen + TypeInfo + Default + codec::FullCodec;
#[pallet::constant]
type PalletId: Get<frame_support::PalletId>;
#[pallet::constant]
type MaxPointsToBalance: Get<u8>;
#[pallet::constant]
type MaxUnbonding: Get<u32>;
type BalanceToU256: Convert<BalanceOf<Self>, U256>;
type U256ToBalance: Convert<U256, BalanceOf<Self>>;
type Staking: StakingInterface<Balance = BalanceOf<Self>, AccountId = Self::AccountId>;
type PostUnbondingPoolsWindow: Get<u32>;
type MaxMetadataLen: Get<u32>;
}
#[pallet::storage]
pub type TotalValueLocked<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
pub type MinJoinBond<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
pub type MinCreateBond<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
pub type MaxPools<T: Config> = StorageValue<_, u32, OptionQuery>;
#[pallet::storage]
pub type MaxPoolMembers<T: Config> = StorageValue<_, u32, OptionQuery>;
#[pallet::storage]
pub type MaxPoolMembersPerPool<T: Config> = StorageValue<_, u32, OptionQuery>;
#[pallet::storage]
pub type GlobalMaxCommission<T: Config> = StorageValue<_, Perbill, OptionQuery>;
#[pallet::storage]
pub type PoolMembers<T: Config> =
CountedStorageMap<_, Twox64Concat, T::AccountId, PoolMember<T>>;
#[pallet::storage]
pub type BondedPools<T: Config> =
CountedStorageMap<_, Twox64Concat, PoolId, BondedPoolInner<T>>;
#[pallet::storage]
pub type RewardPools<T: Config> = CountedStorageMap<_, Twox64Concat, PoolId, RewardPool<T>>;
#[pallet::storage]
pub type SubPoolsStorage<T: Config> = CountedStorageMap<_, Twox64Concat, PoolId, SubPools<T>>;
#[pallet::storage]
pub type Metadata<T: Config> =
CountedStorageMap<_, Twox64Concat, PoolId, BoundedVec<u8, T::MaxMetadataLen>, ValueQuery>;
#[pallet::storage]
pub type LastPoolId<T: Config> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
pub type ReversePoolIdLookup<T: Config> =
CountedStorageMap<_, Twox64Concat, T::AccountId, PoolId, OptionQuery>;
#[pallet::storage]
pub type ClaimPermissions<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, ClaimPermission, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub min_join_bond: BalanceOf<T>,
pub min_create_bond: BalanceOf<T>,
pub max_pools: Option<u32>,
pub max_members_per_pool: Option<u32>,
pub max_members: Option<u32>,
pub global_max_commission: Option<Perbill>,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
min_join_bond: Zero::zero(),
min_create_bond: Zero::zero(),
max_pools: Some(16),
max_members_per_pool: Some(32),
max_members: Some(16 * 32),
global_max_commission: None,
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
MinJoinBond::<T>::put(self.min_join_bond);
MinCreateBond::<T>::put(self.min_create_bond);
if let Some(max_pools) = self.max_pools {
MaxPools::<T>::put(max_pools);
}
if let Some(max_members_per_pool) = self.max_members_per_pool {
MaxPoolMembersPerPool::<T>::put(max_members_per_pool);
}
if let Some(max_members) = self.max_members {
MaxPoolMembers::<T>::put(max_members);
}
if let Some(global_max_commission) = self.global_max_commission {
GlobalMaxCommission::<T>::put(global_max_commission);
}
}
}
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
Created { depositor: T::AccountId, pool_id: PoolId },
Bonded { member: T::AccountId, pool_id: PoolId, bonded: BalanceOf<T>, joined: bool },
PaidOut { member: T::AccountId, pool_id: PoolId, payout: BalanceOf<T> },
Unbonded {
member: T::AccountId,
pool_id: PoolId,
balance: BalanceOf<T>,
points: BalanceOf<T>,
era: EraIndex,
},
Withdrawn {
member: T::AccountId,
pool_id: PoolId,
balance: BalanceOf<T>,
points: BalanceOf<T>,
},
Destroyed { pool_id: PoolId },
StateChanged { pool_id: PoolId, new_state: PoolState },
MemberRemoved { pool_id: PoolId, member: T::AccountId },
RolesUpdated {
root: Option<T::AccountId>,
bouncer: Option<T::AccountId>,
nominator: Option<T::AccountId>,
},
PoolSlashed { pool_id: PoolId, balance: BalanceOf<T> },
UnbondingPoolSlashed { pool_id: PoolId, era: EraIndex, balance: BalanceOf<T> },
PoolCommissionUpdated { pool_id: PoolId, current: Option<(Perbill, T::AccountId)> },
PoolMaxCommissionUpdated { pool_id: PoolId, max_commission: Perbill },
PoolCommissionChangeRateUpdated {
pool_id: PoolId,
change_rate: CommissionChangeRate<BlockNumberFor<T>>,
},
PoolCommissionClaimPermissionUpdated {
pool_id: PoolId,
permission: Option<CommissionClaimPermission<T::AccountId>>,
},
PoolCommissionClaimed { pool_id: PoolId, commission: BalanceOf<T> },
MinBalanceDeficitAdjusted { pool_id: PoolId, amount: BalanceOf<T> },
MinBalanceExcessAdjusted { pool_id: PoolId, amount: BalanceOf<T> },
}
#[pallet::error]
#[cfg_attr(test, derive(PartialEq))]
pub enum Error<T> {
PoolNotFound,
PoolMemberNotFound,
RewardPoolNotFound,
SubPoolsNotFound,
AccountBelongsToOtherPool,
FullyUnbonding,
MaxUnbondingLimit,
CannotWithdrawAny,
MinimumBondNotMet,
OverflowRisk,
NotDestroying,
NotNominator,
NotKickerOrDestroying,
NotOpen,
MaxPools,
MaxPoolMembers,
CanNotChangeState,
DoesNotHavePermission,
MetadataExceedsMaxLen,
Defensive(DefensiveError),
PartialUnbondNotAllowedPermissionlessly,
MaxCommissionRestricted,
CommissionExceedsMaximum,
CommissionExceedsGlobalMaximum,
CommissionChangeThrottled,
CommissionChangeRateNotAllowed,
NoPendingCommission,
NoCommissionCurrentSet,
PoolIdInUse,
InvalidPoolId,
BondExtraRestricted,
NothingToAdjust,
}
#[derive(Encode, Decode, PartialEq, TypeInfo, PalletError, RuntimeDebug)]
pub enum DefensiveError {
NotEnoughSpaceInUnbondPool,
PoolNotFound,
RewardPoolNotFound,
SubPoolsNotFound,
BondedStashKilledPrematurely,
}
impl<T> From<DefensiveError> for Error<T> {
fn from(e: DefensiveError) -> Error<T> {
Error::<T>::Defensive(e)
}
}
#[pallet::composite_enum]
pub enum FreezeReason {
#[codec(index = 0)]
PoolMinBalance,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::join())]
pub fn join(
origin: OriginFor<T>,
#[pallet::compact] amount: BalanceOf<T>,
pool_id: PoolId,
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(amount >= MinJoinBond::<T>::get(), Error::<T>::MinimumBondNotMet);
ensure!(!PoolMembers::<T>::contains_key(&who), Error::<T>::AccountBelongsToOtherPool);
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
bonded_pool.ok_to_join()?;
let mut reward_pool = RewardPools::<T>::get(pool_id)
.defensive_ok_or::<Error<T>>(DefensiveError::RewardPoolNotFound.into())?;
reward_pool.update_records(
pool_id,
bonded_pool.points,
bonded_pool.commission.current(),
)?;
bonded_pool.try_inc_members()?;
let points_issued = bonded_pool.try_bond_funds(&who, amount, BondType::Later)?;
PoolMembers::insert(
who.clone(),
PoolMember::<T> {
pool_id,
points: points_issued,
last_recorded_reward_counter: reward_pool.last_recorded_reward_counter(),
unbonding_eras: Default::default(),
},
);
Self::deposit_event(Event::<T>::Bonded {
member: who,
pool_id,
bonded: amount,
joined: true,
});
bonded_pool.put();
RewardPools::<T>::insert(pool_id, reward_pool);
Ok(())
}
#[pallet::call_index(1)]
#[pallet::weight(
T::WeightInfo::bond_extra_transfer()
.max(T::WeightInfo::bond_extra_other())
)]
pub fn bond_extra(origin: OriginFor<T>, extra: BondExtra<BalanceOf<T>>) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_bond_extra(who.clone(), who, extra)
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::claim_payout())]
pub fn claim_payout(origin: OriginFor<T>) -> DispatchResult {
let signer = ensure_signed(origin)?;
Self::do_claim_payout(signer.clone(), signer)
}
#[pallet::call_index(3)]
#[pallet::weight(T::WeightInfo::unbond())]
pub fn unbond(
origin: OriginFor<T>,
member_account: AccountIdLookupOf<T>,
#[pallet::compact] unbonding_points: BalanceOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let member_account = T::Lookup::lookup(member_account)?;
let (mut member, mut bonded_pool, mut reward_pool) =
Self::get_member_with_pools(&member_account)?;
bonded_pool.ok_to_unbond_with(&who, &member_account, &member, unbonding_points)?;
reward_pool.update_records(
bonded_pool.id,
bonded_pool.points,
bonded_pool.commission.current(),
)?;
let _ = Self::do_reward_payout(&who, &mut member, &mut bonded_pool, &mut reward_pool)?;
let current_era = T::Staking::current_era();
let unbond_era = T::Staking::bonding_duration().saturating_add(current_era);
let unbonding_balance = bonded_pool.dissolve(unbonding_points);
T::Staking::unbond(&bonded_pool.bonded_account(), unbonding_balance)?;
let mut sub_pools = SubPoolsStorage::<T>::get(member.pool_id)
.unwrap_or_default()
.maybe_merge_pools(current_era);
if !sub_pools.with_era.contains_key(&unbond_era) {
sub_pools
.with_era
.try_insert(unbond_era, UnbondPool::default())
.defensive_map_err::<Error<T>, _>(|_| {
DefensiveError::NotEnoughSpaceInUnbondPool.into()
})?;
}
let points_unbonded = sub_pools
.with_era
.get_mut(&unbond_era)
.defensive_ok_or::<Error<T>>(DefensiveError::PoolNotFound.into())?
.issue(unbonding_balance);
member.try_unbond(unbonding_points, points_unbonded, unbond_era)?;
Self::deposit_event(Event::<T>::Unbonded {
member: member_account.clone(),
pool_id: member.pool_id,
points: points_unbonded,
balance: unbonding_balance,
era: unbond_era,
});
SubPoolsStorage::insert(member.pool_id, sub_pools);
Self::put_member_with_pools(&member_account, member, bonded_pool, reward_pool);
Ok(())
}
#[pallet::call_index(4)]
#[pallet::weight(T::WeightInfo::pool_withdraw_unbonded(*num_slashing_spans))]
pub fn pool_withdraw_unbonded(
origin: OriginFor<T>,
pool_id: PoolId,
num_slashing_spans: u32,
) -> DispatchResult {
let _ = ensure_signed(origin)?;
let pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(pool.state != PoolState::Destroying, Error::<T>::NotDestroying);
pool.withdraw_from_staking(num_slashing_spans)?;
Ok(())
}
#[pallet::call_index(5)]
#[pallet::weight(
T::WeightInfo::withdraw_unbonded_kill(*num_slashing_spans)
)]
pub fn withdraw_unbonded(
origin: OriginFor<T>,
member_account: AccountIdLookupOf<T>,
num_slashing_spans: u32,
) -> DispatchResultWithPostInfo {
let caller = ensure_signed(origin)?;
let member_account = T::Lookup::lookup(member_account)?;
let mut member =
PoolMembers::<T>::get(&member_account).ok_or(Error::<T>::PoolMemberNotFound)?;
let current_era = T::Staking::current_era();
let bonded_pool = BondedPool::<T>::get(member.pool_id)
.defensive_ok_or::<Error<T>>(DefensiveError::PoolNotFound.into())?;
let mut sub_pools =
SubPoolsStorage::<T>::get(member.pool_id).ok_or(Error::<T>::SubPoolsNotFound)?;
bonded_pool.ok_to_withdraw_unbonded_with(&caller, &member_account)?;
let withdrawn_points = member.withdraw_unlocked(current_era);
ensure!(!withdrawn_points.is_empty(), Error::<T>::CannotWithdrawAny);
let stash_killed = bonded_pool.withdraw_from_staking(num_slashing_spans)?;
ensure!(
!stash_killed || caller == bonded_pool.roles.depositor,
Error::<T>::Defensive(DefensiveError::BondedStashKilledPrematurely)
);
let mut sum_unlocked_points: BalanceOf<T> = Zero::zero();
let balance_to_unbond = withdrawn_points
.iter()
.fold(BalanceOf::<T>::zero(), |accumulator, (era, unlocked_points)| {
sum_unlocked_points = sum_unlocked_points.saturating_add(*unlocked_points);
if let Some(era_pool) = sub_pools.with_era.get_mut(era) {
let balance_to_unbond = era_pool.dissolve(*unlocked_points);
if era_pool.points.is_zero() {
sub_pools.with_era.remove(era);
}
accumulator.saturating_add(balance_to_unbond)
} else {
accumulator.saturating_add(sub_pools.no_era.dissolve(*unlocked_points))
}
})
.min(bonded_pool.transferable_balance());
T::Currency::transfer(
&bonded_pool.bonded_account(),
&member_account,
balance_to_unbond,
Preservation::Expendable,
)
.defensive()?;
Self::deposit_event(Event::<T>::Withdrawn {
member: member_account.clone(),
pool_id: member.pool_id,
points: sum_unlocked_points,
balance: balance_to_unbond,
});
let post_info_weight = if member.total_points().is_zero() {
ClaimPermissions::<T>::remove(&member_account);
PoolMembers::<T>::remove(&member_account);
Self::deposit_event(Event::<T>::MemberRemoved {
pool_id: member.pool_id,
member: member_account.clone(),
});
if member_account == bonded_pool.roles.depositor {
Pallet::<T>::dissolve_pool(bonded_pool);
None
} else {
bonded_pool.dec_members().put();
SubPoolsStorage::<T>::insert(member.pool_id, sub_pools);
Some(T::WeightInfo::withdraw_unbonded_update(num_slashing_spans))
}
} else {
SubPoolsStorage::<T>::insert(member.pool_id, sub_pools);
PoolMembers::<T>::insert(&member_account, member);
Some(T::WeightInfo::withdraw_unbonded_update(num_slashing_spans))
};
Ok(post_info_weight.into())
}
#[pallet::call_index(6)]
#[pallet::weight(T::WeightInfo::create())]
pub fn create(
origin: OriginFor<T>,
#[pallet::compact] amount: BalanceOf<T>,
root: AccountIdLookupOf<T>,
nominator: AccountIdLookupOf<T>,
bouncer: AccountIdLookupOf<T>,
) -> DispatchResult {
let depositor = ensure_signed(origin)?;
let pool_id = LastPoolId::<T>::try_mutate::<_, Error<T>, _>(|id| {
*id = id.checked_add(1).ok_or(Error::<T>::OverflowRisk)?;
Ok(*id)
})?;
Self::do_create(depositor, amount, root, nominator, bouncer, pool_id)
}
#[pallet::call_index(7)]
#[pallet::weight(T::WeightInfo::create())]
pub fn create_with_pool_id(
origin: OriginFor<T>,
#[pallet::compact] amount: BalanceOf<T>,
root: AccountIdLookupOf<T>,
nominator: AccountIdLookupOf<T>,
bouncer: AccountIdLookupOf<T>,
pool_id: PoolId,
) -> DispatchResult {
let depositor = ensure_signed(origin)?;
ensure!(!BondedPools::<T>::contains_key(pool_id), Error::<T>::PoolIdInUse);
ensure!(pool_id < LastPoolId::<T>::get(), Error::<T>::InvalidPoolId);
Self::do_create(depositor, amount, root, nominator, bouncer, pool_id)
}
#[pallet::call_index(8)]
#[pallet::weight(T::WeightInfo::nominate(validators.len() as u32))]
pub fn nominate(
origin: OriginFor<T>,
pool_id: PoolId,
validators: Vec<T::AccountId>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_nominate(&who), Error::<T>::NotNominator);
T::Staking::nominate(&bonded_pool.bonded_account(), validators)
}
#[pallet::call_index(9)]
#[pallet::weight(T::WeightInfo::set_state())]
pub fn set_state(
origin: OriginFor<T>,
pool_id: PoolId,
state: PoolState,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.state != PoolState::Destroying, Error::<T>::CanNotChangeState);
if bonded_pool.can_toggle_state(&who) {
bonded_pool.set_state(state);
} else if bonded_pool.ok_to_be_open().is_err() && state == PoolState::Destroying {
bonded_pool.set_state(PoolState::Destroying);
} else {
Err(Error::<T>::CanNotChangeState)?;
}
bonded_pool.put();
Ok(())
}
#[pallet::call_index(10)]
#[pallet::weight(T::WeightInfo::set_metadata(metadata.len() as u32))]
pub fn set_metadata(
origin: OriginFor<T>,
pool_id: PoolId,
metadata: Vec<u8>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let metadata: BoundedVec<_, _> =
metadata.try_into().map_err(|_| Error::<T>::MetadataExceedsMaxLen)?;
ensure!(
BondedPool::<T>::get(pool_id)
.ok_or(Error::<T>::PoolNotFound)?
.can_set_metadata(&who),
Error::<T>::DoesNotHavePermission
);
Metadata::<T>::mutate(pool_id, |pool_meta| *pool_meta = metadata);
Ok(())
}
#[pallet::call_index(11)]
#[pallet::weight(T::WeightInfo::set_configs())]
pub fn set_configs(
origin: OriginFor<T>,
min_join_bond: ConfigOp<BalanceOf<T>>,
min_create_bond: ConfigOp<BalanceOf<T>>,
max_pools: ConfigOp<u32>,
max_members: ConfigOp<u32>,
max_members_per_pool: ConfigOp<u32>,
global_max_commission: ConfigOp<Perbill>,
) -> DispatchResult {
ensure_root(origin)?;
macro_rules! config_op_exp {
($storage:ty, $op:ident) => {
match $op {
ConfigOp::Noop => (),
ConfigOp::Set(v) => <$storage>::put(v),
ConfigOp::Remove => <$storage>::kill(),
}
};
}
config_op_exp!(MinJoinBond::<T>, min_join_bond);
config_op_exp!(MinCreateBond::<T>, min_create_bond);
config_op_exp!(MaxPools::<T>, max_pools);
config_op_exp!(MaxPoolMembers::<T>, max_members);
config_op_exp!(MaxPoolMembersPerPool::<T>, max_members_per_pool);
config_op_exp!(GlobalMaxCommission::<T>, global_max_commission);
Ok(())
}
#[pallet::call_index(12)]
#[pallet::weight(T::WeightInfo::update_roles())]
pub fn update_roles(
origin: OriginFor<T>,
pool_id: PoolId,
new_root: ConfigOp<T::AccountId>,
new_nominator: ConfigOp<T::AccountId>,
new_bouncer: ConfigOp<T::AccountId>,
) -> DispatchResult {
let mut bonded_pool = match ensure_root(origin.clone()) {
Ok(()) => BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?,
Err(frame_support::error::BadOrigin) => {
let who = ensure_signed(origin)?;
let bonded_pool =
BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_update_roles(&who), Error::<T>::DoesNotHavePermission);
bonded_pool
},
};
match new_root {
ConfigOp::Noop => (),
ConfigOp::Remove => bonded_pool.roles.root = None,
ConfigOp::Set(v) => bonded_pool.roles.root = Some(v),
};
match new_nominator {
ConfigOp::Noop => (),
ConfigOp::Remove => bonded_pool.roles.nominator = None,
ConfigOp::Set(v) => bonded_pool.roles.nominator = Some(v),
};
match new_bouncer {
ConfigOp::Noop => (),
ConfigOp::Remove => bonded_pool.roles.bouncer = None,
ConfigOp::Set(v) => bonded_pool.roles.bouncer = Some(v),
};
Self::deposit_event(Event::<T>::RolesUpdated {
root: bonded_pool.roles.root.clone(),
nominator: bonded_pool.roles.nominator.clone(),
bouncer: bonded_pool.roles.bouncer.clone(),
});
bonded_pool.put();
Ok(())
}
#[pallet::call_index(13)]
#[pallet::weight(T::WeightInfo::chill())]
pub fn chill(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
let who = ensure_signed(origin)?;
let bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_nominate(&who), Error::<T>::NotNominator);
T::Staking::chill(&bonded_pool.bonded_account())
}
#[pallet::call_index(14)]
#[pallet::weight(
T::WeightInfo::bond_extra_transfer()
.max(T::WeightInfo::bond_extra_other())
)]
pub fn bond_extra_other(
origin: OriginFor<T>,
member: AccountIdLookupOf<T>,
extra: BondExtra<BalanceOf<T>>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_bond_extra(who, T::Lookup::lookup(member)?, extra)
}
#[pallet::call_index(15)]
#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
pub fn set_claim_permission(
origin: OriginFor<T>,
permission: ClaimPermission,
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(PoolMembers::<T>::contains_key(&who), Error::<T>::PoolMemberNotFound);
ClaimPermissions::<T>::mutate(who, |source| {
*source = permission;
});
Ok(())
}
#[pallet::call_index(16)]
#[pallet::weight(T::WeightInfo::claim_payout())]
pub fn claim_payout_other(origin: OriginFor<T>, other: T::AccountId) -> DispatchResult {
let signer = ensure_signed(origin)?;
Self::do_claim_payout(signer, other)
}
#[pallet::call_index(17)]
#[pallet::weight(T::WeightInfo::set_commission())]
pub fn set_commission(
origin: OriginFor<T>,
pool_id: PoolId,
new_commission: Option<(Perbill, T::AccountId)>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_manage_commission(&who), Error::<T>::DoesNotHavePermission);
let mut reward_pool = RewardPools::<T>::get(pool_id)
.defensive_ok_or::<Error<T>>(DefensiveError::RewardPoolNotFound.into())?;
reward_pool.update_records(
pool_id,
bonded_pool.points,
bonded_pool.commission.current(),
)?;
RewardPools::insert(pool_id, reward_pool);
bonded_pool.commission.try_update_current(&new_commission)?;
bonded_pool.put();
Self::deposit_event(Event::<T>::PoolCommissionUpdated {
pool_id,
current: new_commission,
});
Ok(())
}
#[pallet::call_index(18)]
#[pallet::weight(T::WeightInfo::set_commission_max())]
pub fn set_commission_max(
origin: OriginFor<T>,
pool_id: PoolId,
max_commission: Perbill,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_manage_commission(&who), Error::<T>::DoesNotHavePermission);
bonded_pool.commission.try_update_max(pool_id, max_commission)?;
bonded_pool.put();
Self::deposit_event(Event::<T>::PoolMaxCommissionUpdated { pool_id, max_commission });
Ok(())
}
#[pallet::call_index(19)]
#[pallet::weight(T::WeightInfo::set_commission_change_rate())]
pub fn set_commission_change_rate(
origin: OriginFor<T>,
pool_id: PoolId,
change_rate: CommissionChangeRate<BlockNumberFor<T>>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_manage_commission(&who), Error::<T>::DoesNotHavePermission);
bonded_pool.commission.try_update_change_rate(change_rate)?;
bonded_pool.put();
Self::deposit_event(Event::<T>::PoolCommissionChangeRateUpdated {
pool_id,
change_rate,
});
Ok(())
}
#[pallet::call_index(20)]
#[pallet::weight(T::WeightInfo::claim_commission())]
pub fn claim_commission(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_claim_commission(who, pool_id)
}
#[pallet::call_index(21)]
#[pallet::weight(T::WeightInfo::adjust_pool_deposit())]
pub fn adjust_pool_deposit(origin: OriginFor<T>, pool_id: PoolId) -> DispatchResult {
let who = ensure_signed(origin)?;
Self::do_adjust_pool_deposit(who, pool_id)
}
#[pallet::call_index(22)]
#[pallet::weight(T::WeightInfo::set_commission_claim_permission())]
pub fn set_commission_claim_permission(
origin: OriginFor<T>,
pool_id: PoolId,
permission: Option<CommissionClaimPermission<T::AccountId>>,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_manage_commission(&who), Error::<T>::DoesNotHavePermission);
bonded_pool.commission.claim_permission = permission.clone();
bonded_pool.put();
Self::deposit_event(Event::<T>::PoolCommissionClaimPermissionUpdated {
pool_id,
permission,
});
Ok(())
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
Self::do_try_state(u8::MAX)
}
fn integrity_test() {
assert!(
T::MaxPointsToBalance::get() > 0,
"Minimum points to balance ratio must be greater than 0"
);
assert!(
T::Staking::bonding_duration() < TotalUnbondingPools::<T>::get(),
"There must be more unbonding pools then the bonding duration /
so a slash can be applied to relevant unboding pools. (We assume /
the bonding duration > slash deffer duration.",
);
}
}
}
impl<T: Config> Pallet<T> {
pub fn depositor_min_bond() -> BalanceOf<T> {
T::Staking::minimum_nominator_bond()
.max(MinCreateBond::<T>::get())
.max(MinJoinBond::<T>::get())
.max(T::Currency::minimum_balance())
}
pub fn dissolve_pool(bonded_pool: BondedPool<T>) {
let reward_account = bonded_pool.reward_account();
let bonded_account = bonded_pool.bonded_account();
ReversePoolIdLookup::<T>::remove(&bonded_account);
RewardPools::<T>::remove(bonded_pool.id);
SubPoolsStorage::<T>::remove(bonded_pool.id);
let _ = Self::unfreeze_pool_deposit(&bonded_pool.reward_account()).defensive();
defensive_assert!(
frame_system::Pallet::<T>::consumers(&reward_account) == 0,
"reward account of dissolving pool should have no consumers"
);
defensive_assert!(
frame_system::Pallet::<T>::consumers(&bonded_account) == 0,
"bonded account of dissolving pool should have no consumers"
);
defensive_assert!(
T::Staking::total_stake(&bonded_account).unwrap_or_default() == Zero::zero(),
"dissolving pool should not have any stake in the staking pallet"
);
let reward_pool_remaining = T::Currency::reducible_balance(
&reward_account,
Preservation::Expendable,
Fortitude::Polite,
);
let _ = T::Currency::transfer(
&reward_account,
&bonded_pool.roles.depositor,
reward_pool_remaining,
Preservation::Expendable,
);
defensive_assert!(
T::Currency::total_balance(&reward_account) == Zero::zero(),
"could not transfer all amount to depositor while dissolving pool"
);
defensive_assert!(
T::Currency::total_balance(&bonded_pool.bonded_account()) == Zero::zero(),
"dissolving pool should not have any balance"
);
T::Currency::set_balance(&reward_account, Zero::zero());
T::Currency::set_balance(&bonded_pool.bonded_account(), Zero::zero());
Self::deposit_event(Event::<T>::Destroyed { pool_id: bonded_pool.id });
Metadata::<T>::remove(bonded_pool.id);
bonded_pool.remove();
}
pub fn create_bonded_account(id: PoolId) -> T::AccountId {
T::PalletId::get().into_sub_account_truncating((AccountType::Bonded, id))
}
pub fn create_reward_account(id: PoolId) -> T::AccountId {
T::PalletId::get().into_sub_account_truncating((AccountType::Reward, id))
}
fn get_member_with_pools(
who: &T::AccountId,
) -> Result<(PoolMember<T>, BondedPool<T>, RewardPool<T>), Error<T>> {
let member = PoolMembers::<T>::get(who).ok_or(Error::<T>::PoolMemberNotFound)?;
let bonded_pool =
BondedPool::<T>::get(member.pool_id).defensive_ok_or(DefensiveError::PoolNotFound)?;
let reward_pool =
RewardPools::<T>::get(member.pool_id).defensive_ok_or(DefensiveError::PoolNotFound)?;
Ok((member, bonded_pool, reward_pool))
}
fn put_member_with_pools(
member_account: &T::AccountId,
member: PoolMember<T>,
bonded_pool: BondedPool<T>,
reward_pool: RewardPool<T>,
) {
bonded_pool.put();
RewardPools::insert(member.pool_id, reward_pool);
PoolMembers::<T>::insert(member_account, member);
}
fn balance_to_point(
current_balance: BalanceOf<T>,
current_points: BalanceOf<T>,
new_funds: BalanceOf<T>,
) -> BalanceOf<T> {
let u256 = T::BalanceToU256::convert;
let balance = T::U256ToBalance::convert;
match (current_balance.is_zero(), current_points.is_zero()) {
(_, true) => new_funds.saturating_mul(POINTS_TO_BALANCE_INIT_RATIO.into()),
(true, false) => {
new_funds.saturating_mul(current_points)
},
(false, false) => {
balance(u256(current_points).saturating_mul(u256(new_funds)))
.div(current_balance)
},
}
}
fn point_to_balance(
current_balance: BalanceOf<T>,
current_points: BalanceOf<T>,
points: BalanceOf<T>,
) -> BalanceOf<T> {
let u256 = T::BalanceToU256::convert;
let balance = T::U256ToBalance::convert;
if current_balance.is_zero() || current_points.is_zero() || points.is_zero() {
return Zero::zero()
}
balance(
u256(current_balance)
.saturating_mul(u256(points))
.div(u256(current_points)),
)
}
fn do_reward_payout(
member_account: &T::AccountId,
member: &mut PoolMember<T>,
bonded_pool: &mut BondedPool<T>,
reward_pool: &mut RewardPool<T>,
) -> Result<BalanceOf<T>, DispatchError> {
debug_assert_eq!(member.pool_id, bonded_pool.id);
ensure!(!member.active_points().is_zero(), Error::<T>::FullyUnbonding);
let (current_reward_counter, _) = reward_pool.current_reward_counter(
bonded_pool.id,
bonded_pool.points,
bonded_pool.commission.current(),
)?;
let pending_rewards = member.pending_rewards(current_reward_counter)?;
if pending_rewards.is_zero() {
return Ok(pending_rewards)
}
member.last_recorded_reward_counter = current_reward_counter;
reward_pool.register_claimed_reward(pending_rewards);
T::Currency::transfer(
&bonded_pool.reward_account(),
member_account,
pending_rewards,
Preservation::Preserve,
)?;
Self::deposit_event(Event::<T>::PaidOut {
member: member_account.clone(),
pool_id: member.pool_id,
payout: pending_rewards,
});
Ok(pending_rewards)
}
fn do_create(
who: T::AccountId,
amount: BalanceOf<T>,
root: AccountIdLookupOf<T>,
nominator: AccountIdLookupOf<T>,
bouncer: AccountIdLookupOf<T>,
pool_id: PoolId,
) -> DispatchResult {
let root = T::Lookup::lookup(root)?;
let nominator = T::Lookup::lookup(nominator)?;
let bouncer = T::Lookup::lookup(bouncer)?;
ensure!(amount >= Pallet::<T>::depositor_min_bond(), Error::<T>::MinimumBondNotMet);
ensure!(
MaxPools::<T>::get().map_or(true, |max_pools| BondedPools::<T>::count() < max_pools),
Error::<T>::MaxPools
);
ensure!(!PoolMembers::<T>::contains_key(&who), Error::<T>::AccountBelongsToOtherPool);
let mut bonded_pool = BondedPool::<T>::new(
pool_id,
PoolRoles {
root: Some(root),
nominator: Some(nominator),
bouncer: Some(bouncer),
depositor: who.clone(),
},
);
bonded_pool.try_inc_members()?;
let points = bonded_pool.try_bond_funds(&who, amount, BondType::Create)?;
T::Currency::transfer(
&who,
&bonded_pool.reward_account(),
T::Currency::minimum_balance(),
Preservation::Expendable,
)?;
Self::freeze_pool_deposit(&bonded_pool.reward_account())?;
PoolMembers::<T>::insert(
who.clone(),
PoolMember::<T> {
pool_id,
points,
last_recorded_reward_counter: Zero::zero(),
unbonding_eras: Default::default(),
},
);
RewardPools::<T>::insert(
pool_id,
RewardPool::<T> {
last_recorded_reward_counter: Zero::zero(),
last_recorded_total_payouts: Zero::zero(),
total_rewards_claimed: Zero::zero(),
total_commission_pending: Zero::zero(),
total_commission_claimed: Zero::zero(),
},
);
ReversePoolIdLookup::<T>::insert(bonded_pool.bonded_account(), pool_id);
Self::deposit_event(Event::<T>::Created { depositor: who.clone(), pool_id });
Self::deposit_event(Event::<T>::Bonded {
member: who,
pool_id,
bonded: amount,
joined: true,
});
bonded_pool.put();
Ok(())
}
fn do_bond_extra(
signer: T::AccountId,
who: T::AccountId,
extra: BondExtra<BalanceOf<T>>,
) -> DispatchResult {
if signer != who {
ensure!(
ClaimPermissions::<T>::get(&who).can_bond_extra(),
Error::<T>::DoesNotHavePermission
);
ensure!(extra == BondExtra::Rewards, Error::<T>::BondExtraRestricted);
}
let (mut member, mut bonded_pool, mut reward_pool) = Self::get_member_with_pools(&who)?;
reward_pool.update_records(
bonded_pool.id,
bonded_pool.points,
bonded_pool.commission.current(),
)?;
let claimed =
Self::do_reward_payout(&who, &mut member, &mut bonded_pool, &mut reward_pool)?;
let (points_issued, bonded) = match extra {
BondExtra::FreeBalance(amount) =>
(bonded_pool.try_bond_funds(&who, amount, BondType::Later)?, amount),
BondExtra::Rewards =>
(bonded_pool.try_bond_funds(&who, claimed, BondType::Later)?, claimed),
};
bonded_pool.ok_to_be_open()?;
member.points =
member.points.checked_add(&points_issued).ok_or(Error::<T>::OverflowRisk)?;
Self::deposit_event(Event::<T>::Bonded {
member: who.clone(),
pool_id: member.pool_id,
bonded,
joined: false,
});
Self::put_member_with_pools(&who, member, bonded_pool, reward_pool);
Ok(())
}
fn do_claim_commission(who: T::AccountId, pool_id: PoolId) -> DispatchResult {
let bonded_pool = BondedPool::<T>::get(pool_id).ok_or(Error::<T>::PoolNotFound)?;
ensure!(bonded_pool.can_claim_commission(&who), Error::<T>::DoesNotHavePermission);
let mut reward_pool = RewardPools::<T>::get(pool_id)
.defensive_ok_or::<Error<T>>(DefensiveError::RewardPoolNotFound.into())?;
reward_pool.update_records(
pool_id,
bonded_pool.points,
bonded_pool.commission.current(),
)?;
let commission = reward_pool.total_commission_pending;
ensure!(!commission.is_zero(), Error::<T>::NoPendingCommission);
let payee = bonded_pool
.commission
.current
.as_ref()
.map(|(_, p)| p.clone())
.ok_or(Error::<T>::NoCommissionCurrentSet)?;
T::Currency::transfer(
&bonded_pool.reward_account(),
&payee,
commission,
Preservation::Preserve,
)?;
reward_pool.total_commission_claimed =
reward_pool.total_commission_claimed.saturating_add(commission);
reward_pool.total_commission_pending = Zero::zero();
RewardPools::<T>::insert(pool_id, reward_pool);
Self::deposit_event(Event::<T>::PoolCommissionClaimed { pool_id, commission });
Ok(())
}
fn do_claim_payout(signer: T::AccountId, who: T::AccountId) -> DispatchResult {
if signer != who {
ensure!(
ClaimPermissions::<T>::get(&who).can_claim_payout(),
Error::<T>::DoesNotHavePermission
);
}
let (mut member, mut bonded_pool, mut reward_pool) = Self::get_member_with_pools(&who)?;
let _ = Self::do_reward_payout(&who, &mut member, &mut bonded_pool, &mut reward_pool)?;
Self::put_member_with_pools(&who, member, bonded_pool, reward_pool);
Ok(())
}
fn do_adjust_pool_deposit(who: T::AccountId, pool: PoolId) -> DispatchResult {
let bonded_pool = BondedPool::<T>::get(pool).ok_or(Error::<T>::PoolNotFound)?;
let reward_acc = &bonded_pool.reward_account();
let pre_frozen_balance =
T::Currency::balance_frozen(&FreezeReason::PoolMinBalance.into(), reward_acc);
let min_balance = T::Currency::minimum_balance();
if pre_frozen_balance == min_balance {
return Err(Error::<T>::NothingToAdjust.into())
}
Self::freeze_pool_deposit(reward_acc)?;
if pre_frozen_balance > min_balance {
let excess = pre_frozen_balance.saturating_sub(min_balance);
T::Currency::transfer(reward_acc, &who, excess, Preservation::Preserve)?;
Self::deposit_event(Event::<T>::MinBalanceExcessAdjusted {
pool_id: pool,
amount: excess,
});
} else {
let deficit = min_balance.saturating_sub(pre_frozen_balance);
T::Currency::transfer(&who, reward_acc, deficit, Preservation::Expendable)?;
Self::deposit_event(Event::<T>::MinBalanceDeficitAdjusted {
pool_id: pool,
amount: deficit,
});
}
Ok(())
}
pub(crate) fn freeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult {
T::Currency::set_freeze(
&FreezeReason::PoolMinBalance.into(),
reward_acc,
T::Currency::minimum_balance(),
)
}
pub fn unfreeze_pool_deposit(reward_acc: &T::AccountId) -> DispatchResult {
T::Currency::thaw(&FreezeReason::PoolMinBalance.into(), reward_acc)
}
#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))]
pub fn do_try_state(level: u8) -> Result<(), TryRuntimeError> {
if level.is_zero() {
return Ok(())
}
let bonded_pools = BondedPools::<T>::iter_keys().collect::<Vec<_>>();
let reward_pools = RewardPools::<T>::iter_keys().collect::<Vec<_>>();
ensure!(
bonded_pools == reward_pools,
"`BondedPools` and `RewardPools` must all have the EXACT SAME key-set."
);
ensure!(
SubPoolsStorage::<T>::iter_keys().all(|k| bonded_pools.contains(&k)),
"`SubPoolsStorage` must be a subset of the above superset."
);
ensure!(
Metadata::<T>::iter_keys().all(|k| bonded_pools.contains(&k)),
"`Metadata` keys must be a subset of the above superset."
);
ensure!(
MaxPools::<T>::get().map_or(true, |max| bonded_pools.len() <= (max as usize)),
Error::<T>::MaxPools
);
for id in reward_pools {
let account = Self::create_reward_account(id);
if T::Currency::reducible_balance(&account, Preservation::Expendable, Fortitude::Polite) <
T::Currency::minimum_balance()
{
log!(
warn,
"reward pool of {:?}: {:?} (ed = {:?}), should only happen because ED has \
changed recently. Pool operators should be notified to top up the reward \
account",
id,
T::Currency::reducible_balance(
&account,
Preservation::Expendable,
Fortitude::Polite
),
T::Currency::minimum_balance(),
)
}
}
let mut pools_members = BTreeMap::<PoolId, u32>::new();
let mut pools_members_pending_rewards = BTreeMap::<PoolId, BalanceOf<T>>::new();
let mut all_members = 0u32;
let mut total_balance_members = Default::default();
PoolMembers::<T>::iter().try_for_each(|(_, d)| -> Result<(), TryRuntimeError> {
let bonded_pool = BondedPools::<T>::get(d.pool_id).unwrap();
ensure!(!d.total_points().is_zero(), "No member should have zero points");
*pools_members.entry(d.pool_id).or_default() += 1;
all_members += 1;
let reward_pool = RewardPools::<T>::get(d.pool_id).unwrap();
if !bonded_pool.points.is_zero() {
let commission = bonded_pool.commission.current();
let (current_rc, _) = reward_pool
.current_reward_counter(d.pool_id, bonded_pool.points, commission)
.unwrap();
let pending_rewards = d.pending_rewards(current_rc).unwrap();
*pools_members_pending_rewards.entry(d.pool_id).or_default() += pending_rewards;
} total_balance_members += d.total_balance();
Ok(())
})?;
RewardPools::<T>::iter_keys().try_for_each(|id| -> Result<(), TryRuntimeError> {
let pending_rewards_lt_leftover_bal = RewardPool::<T>::current_balance(id) >=
pools_members_pending_rewards.get(&id).copied().unwrap_or_default();
if !pending_rewards_lt_leftover_bal {
log::warn!(
"pool {:?}, sum pending rewards = {:?}, remaining balance = {:?}",
id,
pools_members_pending_rewards.get(&id),
RewardPool::<T>::current_balance(id)
);
}
Ok(())
})?;
let mut expected_tvl: BalanceOf<T> = Default::default();
BondedPools::<T>::iter().try_for_each(|(id, inner)| -> Result<(), TryRuntimeError> {
let bonded_pool = BondedPool { id, inner };
ensure!(
pools_members.get(&id).copied().unwrap_or_default() ==
bonded_pool.member_counter,
"Each `BondedPool.member_counter` must be equal to the actual count of members of this pool"
);
ensure!(
MaxPoolMembersPerPool::<T>::get()
.map_or(true, |max| bonded_pool.member_counter <= max),
Error::<T>::MaxPoolMembers
);
let depositor = PoolMembers::<T>::get(&bonded_pool.roles.depositor).unwrap();
ensure!(
bonded_pool.is_destroying_and_only_depositor(depositor.active_points()) ||
depositor.active_points() >= MinCreateBond::<T>::get(),
"depositor must always have MinCreateBond stake in the pool, except for when the \
pool is being destroyed and the depositor is the last member",
);
expected_tvl +=
T::Staking::total_stake(&bonded_pool.bonded_account()).unwrap_or_default();
Ok(())
})?;
ensure!(
MaxPoolMembers::<T>::get().map_or(true, |max| all_members <= max),
Error::<T>::MaxPoolMembers
);
ensure!(
TotalValueLocked::<T>::get() == expected_tvl,
"TVL deviates from the actual sum of funds of all Pools."
);
ensure!(
TotalValueLocked::<T>::get() <= total_balance_members,
"TVL must be equal to or less than the total balance of all PoolMembers."
);
if level <= 1 {
return Ok(())
}
for (pool_id, _pool) in BondedPools::<T>::iter() {
let pool_account = Pallet::<T>::create_bonded_account(pool_id);
let subs = SubPoolsStorage::<T>::get(pool_id).unwrap_or_default();
let sum_unbonding_balance = subs.sum_unbonding_balance();
let bonded_balance = T::Staking::active_stake(&pool_account).unwrap_or_default();
let total_balance = T::Currency::total_balance(&pool_account);
assert!(
total_balance >= bonded_balance + sum_unbonding_balance,
"faulty pool: {:?} / {:?}, total_balance {:?} >= bonded_balance {:?} + sum_unbonding_balance {:?}",
pool_id,
_pool,
total_balance,
bonded_balance,
sum_unbonding_balance
);
}
let _ = Self::check_ed_imbalance()?;
Ok(())
}
#[cfg(any(feature = "try-runtime", feature = "runtime-benchmarks", test, debug_assertions))]
pub fn check_ed_imbalance() -> Result<(), DispatchError> {
let mut failed: u32 = 0;
BondedPools::<T>::iter_keys().for_each(|id| {
let reward_acc = Self::create_reward_account(id);
let frozen_balance =
T::Currency::balance_frozen(&FreezeReason::PoolMinBalance.into(), &reward_acc);
let expected_frozen_balance = T::Currency::minimum_balance();
if frozen_balance != expected_frozen_balance {
failed += 1;
log::warn!(
"pool {:?} has incorrect ED frozen that can result from change in ED. Expected = {:?}, Actual = {:?}",
id,
expected_frozen_balance,
frozen_balance,
);
}
});
ensure!(failed == 0, "Some pools do not have correct ED frozen");
Ok(())
}
#[cfg(any(feature = "runtime-benchmarks", test))]
pub fn fully_unbond(
origin: frame_system::pallet_prelude::OriginFor<T>,
member: T::AccountId,
) -> DispatchResult {
let points = PoolMembers::<T>::get(&member).map(|d| d.active_points()).unwrap_or_default();
let member_lookup = T::Lookup::unlookup(member);
Self::unbond(origin, member_lookup, points)
}
}
impl<T: Config> Pallet<T> {
pub fn api_pending_rewards(who: T::AccountId) -> Option<BalanceOf<T>> {
if let Some(pool_member) = PoolMembers::<T>::get(who) {
if let Some((reward_pool, bonded_pool)) = RewardPools::<T>::get(pool_member.pool_id)
.zip(BondedPools::<T>::get(pool_member.pool_id))
{
let commission = bonded_pool.commission.current();
let (current_reward_counter, _) = reward_pool
.current_reward_counter(pool_member.pool_id, bonded_pool.points, commission)
.ok()?;
return pool_member.pending_rewards(current_reward_counter).ok()
}
}
None
}
pub fn api_points_to_balance(pool_id: PoolId, points: BalanceOf<T>) -> BalanceOf<T> {
if let Some(pool) = BondedPool::<T>::get(pool_id) {
pool.points_to_balance(points)
} else {
Zero::zero()
}
}
pub fn api_balance_to_points(pool_id: PoolId, new_funds: BalanceOf<T>) -> BalanceOf<T> {
if let Some(pool) = BondedPool::<T>::get(pool_id) {
let bonded_balance =
T::Staking::active_stake(&pool.bonded_account()).unwrap_or(Zero::zero());
Pallet::<T>::balance_to_point(bonded_balance, pool.points, new_funds)
} else {
Zero::zero()
}
}
}
impl<T: Config> sp_staking::OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pallet<T> {
fn on_slash(
pool_account: &T::AccountId,
slashed_bonded: BalanceOf<T>,
slashed_unlocking: &BTreeMap<EraIndex, BalanceOf<T>>,
total_slashed: BalanceOf<T>,
) {
let Some(pool_id) = ReversePoolIdLookup::<T>::get(pool_account) else { return };
TotalValueLocked::<T>::mutate(|tvl| {
tvl.defensive_saturating_reduce(total_slashed);
});
if let Some(mut sub_pools) = SubPoolsStorage::<T>::get(pool_id) {
slashed_unlocking.iter().for_each(|(era, slashed_balance)| {
if let Some(pool) = sub_pools.with_era.get_mut(era).defensive() {
pool.balance = *slashed_balance;
Self::deposit_event(Event::<T>::UnbondingPoolSlashed {
era: *era,
pool_id,
balance: *slashed_balance,
});
}
});
SubPoolsStorage::<T>::insert(pool_id, sub_pools);
} else if !slashed_unlocking.is_empty() {
defensive!("Expected SubPools were not found");
}
Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: slashed_bonded });
}
}