#![cfg_attr(not(feature = "std"), no_std)]
use codec::Codec;
use frame_support::{
defensive, ensure,
pallet_prelude::{MaxEncodedLen, *},
storage::bounded_btree_map::BoundedBTreeMap,
traits::{
Currency, Defensive, DefensiveOption, DefensiveResult, DefensiveSaturating,
ExistenceRequirement, Get,
},
DefaultNoBound,
};
use scale_info::TypeInfo;
use sp_core::U256;
use sp_runtime::{
traits::{
AccountIdConversion, CheckedAdd, CheckedSub, Convert, Saturating, StaticLookup, Zero,
},
FixedPointNumber,
};
use sp_staking::{EraIndex, OnStakerSlash, StakingInterface};
use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, ops::Div, vec::Vec};
pub const LOG_TARGET: &'static 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 Currency<<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, 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()
}
}
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 state_toggler: Option<AccountId>,
}
#[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 points: BalanceOf<T>,
pub state: PoolState,
pub member_counter: u32,
pub roles: PoolRoles<T::AccountId>,
}
#[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 {
roles,
state: PoolState::Open,
points: Zero::zero(),
member_counter: Zero::zero(),
},
}
}
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, BondedPoolInner { ..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 transferrable_balance(&self) -> BalanceOf<T> {
let account = self.bonded_account();
T::Currency::free_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_state_toggler(&self, who: &T::AccountId) -> bool {
self.roles
.state_toggler
.as_ref()
.map_or(false, |state_toggler| state_toggler == 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_state_toggler(who))
}
fn can_toggle_state(&self, who: &T::AccountId) -> bool {
(self.is_root(who) || self.is_state_toggler(who)) && !self.is_destroying()
}
fn can_set_metadata(&self, who: &T::AccountId) -> bool {
self.is_root(who) || self.is_state_toggler(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 => ExistenceRequirement::AllowDeath,
BondType::Later => ExistenceRequirement::KeepAlive,
},
)?;
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)?,
}
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,
});
};
}
}
#[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>,
}
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>) -> Result<(), Error<T>> {
let balance = Self::current_balance(id);
self.last_recorded_reward_counter = self.current_reward_counter(id, bonded_points)?;
self.last_recorded_total_payouts = balance
.checked_add(&self.total_rewards_claimed)
.ok_or(Error::<T>::OverflowRisk)?;
Ok(())
}
fn current_reward_counter(
&self,
id: PoolId,
bonded_points: BalanceOf<T>,
) -> Result<T::RewardCounter, Error<T>> {
let balance = Self::current_balance(id);
let payouts_since_last_record = balance
.saturating_add(self.total_rewards_claimed)
.saturating_sub(self.last_recorded_total_payouts);
T::RewardCounter::checked_from_rational(payouts_since_last_record, bonded_points)
.and_then(|ref r| self.last_recorded_reward_counter.checked_add(r))
.ok_or(Error::<T>::OverflowRisk)
}
fn current_balance(id: PoolId) -> BalanceOf<T> {
T::Currency::free_balance(&Pallet::<T>::create_reward_account(id))
.saturating_sub(T::Currency::minimum_balance())
}
}
#[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::*};
const STORAGE_VERSION: StorageVersion = StorageVersion::new(3);
#[pallet::pallet]
#[pallet::generate_store(pub(crate) trait Store)]
#[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: Currency<Self::AccountId>;
type RewardCounter: FixedPointNumber + MaxEncodedLen + TypeInfo + Default + codec::FullCodec;
#[pallet::constant]
type PalletId: Get<frame_support::PalletId>;
#[pallet::constant]
type MaxPointsToBalance: Get<u8>;
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>;
type MaxUnbonding: Get<u32>;
}
#[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 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::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>,
}
#[cfg(feature = "std")]
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),
}
}
}
#[pallet::genesis_build]
impl<T: Config> GenesisBuild<T> 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);
}
}
}
#[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>,
state_toggler: Option<T::AccountId>,
nominator: Option<T::AccountId>,
},
PoolSlashed { pool_id: PoolId, balance: BalanceOf<T> },
UnbondingPoolSlashed { pool_id: PoolId, era: EraIndex, balance: 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,
PoolIdInUse,
InvalidPoolId,
}
#[derive(Encode, Decode, PartialEq, TypeInfo, frame_support::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::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.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_reward())
)]
pub fn bond_extra(origin: OriginFor<T>, extra: BondExtra<BalanceOf<T>>) -> DispatchResult {
let who = ensure_signed(origin)?;
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)?;
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(())
}
#[pallet::call_index(2)]
#[pallet::weight(T::WeightInfo::claim_payout())]
pub fn claim_payout(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;
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(())
}
#[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)?;
let _ = reward_pool.update_records(bonded_pool.id, bonded_pool.points)?;
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);
T::Staking::withdraw_unbonded(pool.bonded_account(), 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 =
T::Staking::withdraw_unbonded(bonded_pool.bonded_account(), 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.transferrable_balance());
T::Currency::transfer(
&bonded_pool.bonded_account(),
&member_account,
balance_to_unbond,
ExistenceRequirement::AllowDeath,
)
.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() {
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>,
state_toggler: 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, state_toggler, 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>,
state_toggler: 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, state_toggler, 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>,
) -> 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);
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_state_toggler: 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_state_toggler {
ConfigOp::Noop => (),
ConfigOp::Remove => bonded_pool.roles.state_toggler = None,
ConfigOp::Set(v) => bonded_pool.roles.state_toggler = Some(v),
};
Self::deposit_event(Event::<T>::RolesUpdated {
root: bonded_pool.roles.root.clone(),
nominator: bonded_pool.roles.nominator.clone(),
state_toggler: bonded_pool.roles.state_toggler.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::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
#[cfg(feature = "try-runtime")]
fn try_state(_n: BlockNumberFor<T>) -> Result<(), &'static str> {
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 pending_rewards(member_account: T::AccountId) -> Option<BalanceOf<T>> {
if let Some(pool_member) = PoolMembers::<T>::get(member_account) {
if let Some((reward_pool, bonded_pool)) = RewardPools::<T>::get(pool_member.pool_id)
.zip(BondedPools::<T>::get(pool_member.pool_id))
{
let current_reward_counter = reward_pool
.current_reward_counter(pool_member.pool_id, bonded_pool.points)
.ok()?;
return pool_member.pending_rewards(current_reward_counter).ok()
}
}
None
}
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);
debug_assert_eq!(frame_system::Pallet::<T>::consumers(&reward_account), 0);
debug_assert_eq!(frame_system::Pallet::<T>::consumers(&bonded_account), 0);
debug_assert_eq!(
T::Staking::total_stake(&bonded_account).unwrap_or_default(),
Zero::zero()
);
let reward_pool_remaining = T::Currency::free_balance(&reward_account);
let _ = T::Currency::transfer(
&reward_account,
&bonded_pool.roles.depositor,
reward_pool_remaining,
ExistenceRequirement::AllowDeath,
);
T::Currency::make_free_balance_be(&reward_account, Zero::zero());
T::Currency::make_free_balance_be(&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 = |x| T::BalanceToU256::convert(x);
let balance = |x| T::U256ToBalance::convert(x);
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(u256(current_balance)),
)
},
}
}
fn point_to_balance(
current_balance: BalanceOf<T>,
current_points: BalanceOf<T>,
points: BalanceOf<T>,
) -> BalanceOf<T> {
let u256 = |x| T::BalanceToU256::convert(x);
let balance = |x| T::U256ToBalance::convert(x);
if current_balance.is_zero() || current_points.is_zero() || points.is_zero() {
return Zero::zero()
}
balance(u256(current_balance).saturating_mul(u256(points)))
.div(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)?;
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,
ExistenceRequirement::AllowDeath,
)?;
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>,
state_toggler: AccountIdLookupOf<T>,
pool_id: PoolId,
) -> DispatchResult {
let root = T::Lookup::lookup(root)?;
let nominator = T::Lookup::lookup(nominator)?;
let state_toggler = T::Lookup::lookup(state_toggler)?;
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),
state_toggler: Some(state_toggler),
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(),
ExistenceRequirement::AllowDeath,
)?;
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(),
},
);
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(())
}
#[cfg(any(feature = "try-runtime", feature = "fuzzing", test, debug_assertions))]
pub fn do_try_state(level: u8) -> Result<(), &'static str> {
if level.is_zero() {
return Ok(())
}
let bonded_pools = BondedPools::<T>::iter_keys().collect::<Vec<_>>();
let reward_pools = RewardPools::<T>::iter_keys().collect::<Vec<_>>();
assert_eq!(bonded_pools, reward_pools);
assert!(Metadata::<T>::iter_keys().all(|k| bonded_pools.contains(&k)));
assert!(SubPoolsStorage::<T>::iter_keys().all(|k| bonded_pools.contains(&k)));
assert!(MaxPools::<T>::get().map_or(true, |max| bonded_pools.len() <= (max as usize)));
for id in reward_pools {
let account = Self::create_reward_account(id);
assert!(
T::Currency::free_balance(&account) >= T::Currency::minimum_balance(),
"reward pool of {id}: {:?} (ed = {:?})",
T::Currency::free_balance(&account),
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;
PoolMembers::<T>::iter().for_each(|(_, d)| {
let bonded_pool = BondedPools::<T>::get(d.pool_id).unwrap();
assert!(!d.total_points().is_zero(), "no member should have zero points: {:?}", d);
*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 current_rc =
reward_pool.current_reward_counter(d.pool_id, bonded_pool.points).unwrap();
*pools_members_pending_rewards.entry(d.pool_id).or_default() +=
d.pending_rewards(current_rc).unwrap();
} });
RewardPools::<T>::iter_keys().for_each(|id| {
log!(
trace,
"pool {:?}, sum pending rewards = {:?}, remaining balance = {:?}",
id,
pools_members_pending_rewards.get(&id),
RewardPool::<T>::current_balance(id)
);
assert!(
RewardPool::<T>::current_balance(id) >=
pools_members_pending_rewards.get(&id).map(|x| *x).unwrap_or_default()
)
});
BondedPools::<T>::iter().for_each(|(id, inner)| {
let bonded_pool = BondedPool { id, inner };
assert_eq!(
pools_members.get(&id).map(|x| *x).unwrap_or_default(),
bonded_pool.member_counter
);
assert!(MaxPoolMembersPerPool::<T>::get()
.map_or(true, |max| bonded_pool.member_counter <= max));
let depositor = PoolMembers::<T>::get(&bonded_pool.roles.depositor).unwrap();
assert!(
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",
);
});
assert!(MaxPoolMembers::<T>::get().map_or(true, |max| all_members <= max));
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
);
}
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> OnStakerSlash<T::AccountId, BalanceOf<T>> for Pallet<T> {
fn on_slash(
pool_account: &T::AccountId,
slashed_bonded: BalanceOf<T>,
slashed_unlocking: &BTreeMap<EraIndex, BalanceOf<T>>,
) {
if let Some(pool_id) = ReversePoolIdLookup::<T>::get(pool_account) {
let mut sub_pools = match SubPoolsStorage::<T>::get(pool_id).defensive() {
Some(sub_pools) => sub_pools,
None => return,
};
for (era, slashed_balance) in slashed_unlocking.iter() {
if let Some(pool) = sub_pools.with_era.get_mut(era) {
pool.balance = *slashed_balance;
Self::deposit_event(Event::<T>::UnbondingPoolSlashed {
era: *era,
pool_id,
balance: *slashed_balance,
});
}
}
Self::deposit_event(Event::<T>::PoolSlashed { pool_id, balance: slashed_bonded });
SubPoolsStorage::<T>::insert(pool_id, sub_pools);
}
}
}