#![cfg_attr(not(feature = "std"), no_std)]
mod auto_compound;
mod delegation_requests;
pub mod inflation;
pub mod migrations;
pub mod traits;
pub mod types;
pub mod weights;
#[cfg(any(test, feature = "runtime-benchmarks"))]
mod benchmarks;
#[cfg(test)]
mod mock;
mod set;
#[cfg(test)]
mod tests;
use frame_support::pallet;
pub use inflation::{InflationInfo, Range};
pub use weights::WeightInfo;
pub use auto_compound::{AutoCompoundConfig, AutoCompoundDelegations};
pub use delegation_requests::{CancelledScheduledRequest, DelegationAction, ScheduledRequest};
pub use pallet::*;
pub use traits::*;
pub use types::*;
pub use RoundIndex;
#[pallet]
pub mod pallet {
use crate::delegation_requests::{
CancelledScheduledRequest, DelegationAction, ScheduledRequest,
};
use crate::{set::BoundedOrderedSet, traits::*, types::*, InflationInfo, Range, WeightInfo};
use crate::{AutoCompoundConfig, AutoCompoundDelegations};
use frame_support::fail;
use frame_support::pallet_prelude::*;
use frame_support::traits::{
tokens::WithdrawReasons, Currency, Get, Imbalance, LockIdentifier, LockableCurrency,
ReservableCurrency,
};
use frame_system::pallet_prelude::*;
use sp_consensus_slots::Slot;
use sp_runtime::{
traits::{Saturating, Zero},
DispatchErrorWithPostInfo, Perbill, Percent,
};
use sp_std::{collections::btree_map::BTreeMap, prelude::*};
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(PhantomData<T>);
pub type RoundIndex = u32;
type RewardPoint = u32;
pub type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
pub const COLLATOR_LOCK_ID: LockIdentifier = *b"stkngcol";
pub const DELEGATOR_LOCK_ID: LockIdentifier = *b"stkngdel";
pub const MAX_CANDIDATES: u32 = 200;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type Currency: Currency<Self::AccountId>
+ ReservableCurrency<Self::AccountId>
+ LockableCurrency<Self::AccountId>;
type MonetaryGovernanceOrigin: EnsureOrigin<Self::RuntimeOrigin>;
#[pallet::constant]
type MinBlocksPerRound: Get<u32>;
#[pallet::constant]
type MaxOfflineRounds: Get<u32>;
#[pallet::constant]
type LeaveCandidatesDelay: Get<RoundIndex>;
#[pallet::constant]
type CandidateBondLessDelay: Get<RoundIndex>;
#[pallet::constant]
type LeaveDelegatorsDelay: Get<RoundIndex>;
#[pallet::constant]
type RevokeDelegationDelay: Get<RoundIndex>;
#[pallet::constant]
type DelegationBondLessDelay: Get<RoundIndex>;
#[pallet::constant]
type RewardPaymentDelay: Get<RoundIndex>;
#[pallet::constant]
type MinSelectedCandidates: Get<u32>;
#[pallet::constant]
type MaxTopDelegationsPerCandidate: Get<u32>;
#[pallet::constant]
type MaxBottomDelegationsPerCandidate: Get<u32>;
#[pallet::constant]
type MaxDelegationsPerDelegator: Get<u32>;
#[pallet::constant]
type MinCandidateStk: Get<BalanceOf<Self>>;
#[pallet::constant]
type MinDelegation: Get<BalanceOf<Self>>;
type BlockAuthor: Get<Self::AccountId>;
type OnCollatorPayout: OnCollatorPayout<Self::AccountId, BalanceOf<Self>>;
type PayoutCollatorReward: PayoutCollatorReward<Self>;
type OnInactiveCollator: OnInactiveCollator<Self>;
type OnNewRound: OnNewRound;
type SlotProvider: Get<Slot>;
#[pallet::constant]
type SlotDuration: Get<u64>;
#[pallet::constant]
type BlockTime: Get<u64>;
#[pallet::constant]
type MaxCandidates: Get<u32>;
type WeightInfo: WeightInfo;
}
#[pallet::error]
pub enum Error<T> {
DelegatorDNE,
DelegatorDNEinTopNorBottom,
DelegatorDNEInDelegatorSet,
CandidateDNE,
DelegationDNE,
DelegatorExists,
CandidateExists,
CandidateBondBelowMin,
InsufficientBalance,
DelegatorBondBelowMin,
DelegationBelowMin,
AlreadyOffline,
AlreadyActive,
DelegatorAlreadyLeaving,
DelegatorNotLeaving,
DelegatorCannotLeaveYet,
CannotDelegateIfLeaving,
CandidateAlreadyLeaving,
CandidateNotLeaving,
CandidateCannotLeaveYet,
CannotGoOnlineIfLeaving,
ExceedMaxDelegationsPerDelegator,
AlreadyDelegatedCandidate,
InvalidSchedule,
CannotSetBelowMin,
RoundLengthMustBeGreaterThanTotalSelectedCollators,
NoWritingSameValue,
TotalInflationDistributionPercentExceeds100,
TooLowCandidateCountWeightHintJoinCandidates,
TooLowCandidateCountWeightHintCancelLeaveCandidates,
TooLowCandidateCountToLeaveCandidates,
TooLowDelegationCountToDelegate,
TooLowCandidateDelegationCountToDelegate,
TooLowCandidateDelegationCountToLeaveCandidates,
TooLowDelegationCountToLeaveDelegators,
PendingCandidateRequestsDNE,
PendingCandidateRequestAlreadyExists,
PendingCandidateRequestNotDueYet,
PendingDelegationRequestDNE,
PendingDelegationRequestAlreadyExists,
PendingDelegationRequestNotDueYet,
CannotDelegateLessThanOrEqualToLowestBottomWhenFull,
PendingDelegationRevoke,
TooLowDelegationCountToAutoCompound,
TooLowCandidateAutoCompoundingDelegationCountToAutoCompound,
TooLowCandidateAutoCompoundingDelegationCountToDelegate,
TooLowCollatorCountToNotifyAsInactive,
CannotBeNotifiedAsInactive,
TooLowCandidateAutoCompoundingDelegationCountToLeaveCandidates,
TooLowCandidateCountWeightHint,
TooLowCandidateCountWeightHintGoOffline,
CandidateLimitReached,
CannotSetAboveMaxCandidates,
RemovedCall,
MarkingOfflineNotEnabled,
CurrentRoundTooLow,
}
#[pallet::event]
#[pallet::generate_deposit(pub(crate) fn deposit_event)]
pub enum Event<T: Config> {
NewRound {
starting_block: BlockNumberFor<T>,
round: RoundIndex,
selected_collators_number: u32,
total_balance: BalanceOf<T>,
},
JoinedCollatorCandidates {
account: T::AccountId,
amount_locked: BalanceOf<T>,
new_total_amt_locked: BalanceOf<T>,
},
CollatorChosen {
round: RoundIndex,
collator_account: T::AccountId,
total_exposed_amount: BalanceOf<T>,
},
CandidateBondLessRequested {
candidate: T::AccountId,
amount_to_decrease: BalanceOf<T>,
execute_round: RoundIndex,
},
CandidateBondedMore {
candidate: T::AccountId,
amount: BalanceOf<T>,
new_total_bond: BalanceOf<T>,
},
CandidateBondedLess {
candidate: T::AccountId,
amount: BalanceOf<T>,
new_bond: BalanceOf<T>,
},
CandidateWentOffline { candidate: T::AccountId },
CandidateBackOnline { candidate: T::AccountId },
CandidateScheduledExit {
exit_allowed_round: RoundIndex,
candidate: T::AccountId,
scheduled_exit: RoundIndex,
},
CancelledCandidateExit { candidate: T::AccountId },
CancelledCandidateBondLess {
candidate: T::AccountId,
amount: BalanceOf<T>,
execute_round: RoundIndex,
},
CandidateLeft {
ex_candidate: T::AccountId,
unlocked_amount: BalanceOf<T>,
new_total_amt_locked: BalanceOf<T>,
},
DelegationDecreaseScheduled {
delegator: T::AccountId,
candidate: T::AccountId,
amount_to_decrease: BalanceOf<T>,
execute_round: RoundIndex,
},
DelegationIncreased {
delegator: T::AccountId,
candidate: T::AccountId,
amount: BalanceOf<T>,
in_top: bool,
},
DelegationDecreased {
delegator: T::AccountId,
candidate: T::AccountId,
amount: BalanceOf<T>,
in_top: bool,
},
DelegatorExitScheduled {
round: RoundIndex,
delegator: T::AccountId,
scheduled_exit: RoundIndex,
},
DelegationRevocationScheduled {
round: RoundIndex,
delegator: T::AccountId,
candidate: T::AccountId,
scheduled_exit: RoundIndex,
},
DelegatorLeft {
delegator: T::AccountId,
unstaked_amount: BalanceOf<T>,
},
DelegationRevoked {
delegator: T::AccountId,
candidate: T::AccountId,
unstaked_amount: BalanceOf<T>,
},
DelegationKicked {
delegator: T::AccountId,
candidate: T::AccountId,
unstaked_amount: BalanceOf<T>,
},
DelegatorExitCancelled { delegator: T::AccountId },
CancelledDelegationRequest {
delegator: T::AccountId,
cancelled_request: CancelledScheduledRequest<BalanceOf<T>>,
collator: T::AccountId,
},
Delegation {
delegator: T::AccountId,
locked_amount: BalanceOf<T>,
candidate: T::AccountId,
delegator_position: DelegatorAdded<BalanceOf<T>>,
auto_compound: Percent,
},
DelegatorLeftCandidate {
delegator: T::AccountId,
candidate: T::AccountId,
unstaked_amount: BalanceOf<T>,
total_candidate_staked: BalanceOf<T>,
},
Rewarded {
account: T::AccountId,
rewards: BalanceOf<T>,
},
InflationDistributed {
index: u32,
account: T::AccountId,
value: BalanceOf<T>,
},
InflationDistributionConfigUpdated {
old: InflationDistributionConfig<T::AccountId>,
new: InflationDistributionConfig<T::AccountId>,
},
InflationSet {
annual_min: Perbill,
annual_ideal: Perbill,
annual_max: Perbill,
round_min: Perbill,
round_ideal: Perbill,
round_max: Perbill,
},
StakeExpectationsSet {
expect_min: BalanceOf<T>,
expect_ideal: BalanceOf<T>,
expect_max: BalanceOf<T>,
},
TotalSelectedSet { old: u32, new: u32 },
CollatorCommissionSet { old: Perbill, new: Perbill },
BlocksPerRoundSet {
current_round: RoundIndex,
first_block: BlockNumberFor<T>,
old: u32,
new: u32,
new_per_round_inflation_min: Perbill,
new_per_round_inflation_ideal: Perbill,
new_per_round_inflation_max: Perbill,
},
AutoCompoundSet {
candidate: T::AccountId,
delegator: T::AccountId,
value: Percent,
},
Compounded {
candidate: T::AccountId,
delegator: T::AccountId,
amount: BalanceOf<T>,
},
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(n: BlockNumberFor<T>) -> Weight {
let mut weight = <T as Config>::WeightInfo::base_on_initialize();
let mut round = <Round<T>>::get();
if round.should_update(n) {
let current_slot: u64 = T::SlotProvider::get().into();
weight = weight.saturating_add(T::DbWeight::get().reads_writes(1, 0));
let round_duration = (current_slot.saturating_sub(round.first_slot))
.saturating_mul(T::SlotDuration::get());
round.update(n, current_slot);
weight = weight.saturating_add(T::OnNewRound::on_new_round(round.current));
weight =
weight.saturating_add(Self::prepare_staking_payouts(round, round_duration));
let (extra_weight, collator_count, _delegation_count, total_staked) =
Self::select_top_candidates(round.current);
weight = weight.saturating_add(extra_weight);
<Round<T>>::put(round);
Self::deposit_event(Event::NewRound {
starting_block: round.first,
round: round.current,
selected_collators_number: collator_count,
total_balance: total_staked,
});
weight = weight.saturating_add(T::DbWeight::get().reads_writes(0, 1));
} else {
weight = weight.saturating_add(Self::handle_delayed_payouts(round.current));
}
weight = weight.saturating_add(T::DbWeight::get().reads_writes(3, 2));
weight
}
fn on_finalize(_n: BlockNumberFor<T>) {
Self::award_points_to_block_author();
}
}
#[pallet::storage]
#[pallet::getter(fn collator_commission)]
type CollatorCommission<T: Config> = StorageValue<_, Perbill, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn total_selected)]
pub(crate) type TotalSelected<T: Config> = StorageValue<_, u32, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn inflation_distribution_info)]
pub(crate) type InflationDistributionInfo<T: Config> =
StorageValue<_, InflationDistributionConfig<T::AccountId>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn round)]
pub type Round<T: Config> = StorageValue<_, RoundInfo<BlockNumberFor<T>>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn delegator_state)]
pub(crate) type DelegatorState<T: Config> = StorageMap<
_,
Twox64Concat,
T::AccountId,
Delegator<T::AccountId, BalanceOf<T>>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn candidate_info)]
pub(crate) type CandidateInfo<T: Config> =
StorageMap<_, Twox64Concat, T::AccountId, CandidateMetadata<BalanceOf<T>>, OptionQuery>;
pub struct AddGet<T, R> {
_phantom: PhantomData<(T, R)>,
}
impl<T, R> Get<u32> for AddGet<T, R>
where
T: Get<u32>,
R: Get<u32>,
{
fn get() -> u32 {
T::get() + R::get()
}
}
#[pallet::storage]
#[pallet::getter(fn delegation_scheduled_requests)]
pub(crate) type DelegationScheduledRequests<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
BoundedVec<
ScheduledRequest<T::AccountId, BalanceOf<T>>,
AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn auto_compounding_delegations)]
pub(crate) type AutoCompoundingDelegations<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
BoundedVec<
AutoCompoundConfig<T::AccountId>,
AddGet<T::MaxTopDelegationsPerCandidate, T::MaxBottomDelegationsPerCandidate>,
>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn top_delegations)]
pub(crate) type TopDelegations<T: Config> = StorageMap<
_,
Twox64Concat,
T::AccountId,
Delegations<T::AccountId, BalanceOf<T>>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn bottom_delegations)]
pub(crate) type BottomDelegations<T: Config> = StorageMap<
_,
Twox64Concat,
T::AccountId,
Delegations<T::AccountId, BalanceOf<T>>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn selected_candidates)]
type SelectedCandidates<T: Config> =
StorageValue<_, BoundedVec<T::AccountId, T::MaxCandidates>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn total)]
pub(crate) type Total<T: Config> = StorageValue<_, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn candidate_pool)]
pub(crate) type CandidatePool<T: Config> = StorageValue<
_,
BoundedOrderedSet<Bond<T::AccountId, BalanceOf<T>>, T::MaxCandidates>,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn at_stake)]
pub type AtStake<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
RoundIndex,
Twox64Concat,
T::AccountId,
CollatorSnapshot<T::AccountId, BalanceOf<T>>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn delayed_payouts)]
pub type DelayedPayouts<T: Config> =
StorageMap<_, Twox64Concat, RoundIndex, DelayedPayout<BalanceOf<T>>, OptionQuery>;
#[pallet::storage]
#[pallet::getter(fn inflation_config)]
pub type InflationConfig<T: Config> = StorageValue<_, InflationInfo<BalanceOf<T>>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn points)]
pub type Points<T: Config> = StorageMap<_, Twox64Concat, RoundIndex, RewardPoint, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn awarded_pts)]
pub type AwardedPts<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
RoundIndex,
Twox64Concat,
T::AccountId,
RewardPoint,
ValueQuery,
>;
#[pallet::storage]
#[pallet::getter(fn marking_offline)]
pub type EnableMarkingOffline<T: Config> = StorageValue<_, bool, ValueQuery>;
#[pallet::genesis_config]
pub struct GenesisConfig<T: Config> {
pub candidates: Vec<(T::AccountId, BalanceOf<T>)>,
pub delegations: Vec<(T::AccountId, T::AccountId, BalanceOf<T>, Percent)>,
pub inflation_config: InflationInfo<BalanceOf<T>>,
pub collator_commission: Perbill,
pub parachain_bond_reserve_percent: Percent,
pub blocks_per_round: u32,
pub num_selected_candidates: u32,
}
impl<T: Config> Default for GenesisConfig<T> {
fn default() -> Self {
Self {
candidates: vec![],
delegations: vec![],
inflation_config: Default::default(),
collator_commission: Default::default(),
parachain_bond_reserve_percent: Default::default(),
blocks_per_round: 1u32,
num_selected_candidates: T::MinSelectedCandidates::get(),
}
}
}
#[pallet::genesis_build]
impl<T: Config> BuildGenesisConfig for GenesisConfig<T> {
fn build(&self) {
assert!(self.blocks_per_round > 0, "Blocks per round must be > 0");
<InflationConfig<T>>::put(self.inflation_config.clone());
let mut candidate_count = 0u32;
for &(ref candidate, balance) in &self.candidates {
assert!(
<Pallet<T>>::get_collator_stakable_free_balance(candidate) >= balance,
"Account does not have enough balance to bond as a candidate."
);
if let Err(error) = <Pallet<T>>::join_candidates(
T::RuntimeOrigin::from(Some(candidate.clone()).into()),
balance,
candidate_count,
) {
log::warn!("Join candidates failed in genesis with error {:?}", error);
} else {
candidate_count = candidate_count.saturating_add(1u32);
}
}
let mut col_delegator_count: BTreeMap<T::AccountId, u32> = BTreeMap::new();
let mut col_auto_compound_delegator_count: BTreeMap<T::AccountId, u32> =
BTreeMap::new();
let mut del_delegation_count: BTreeMap<T::AccountId, u32> = BTreeMap::new();
for &(ref delegator, ref target, balance, auto_compound) in &self.delegations {
assert!(
<Pallet<T>>::get_delegator_stakable_balance(delegator) >= balance,
"Account does not have enough balance to place delegation."
);
let cd_count = if let Some(x) = col_delegator_count.get(target) {
*x
} else {
0u32
};
let dd_count = if let Some(x) = del_delegation_count.get(delegator) {
*x
} else {
0u32
};
let cd_auto_compound_count = col_auto_compound_delegator_count
.get(target)
.cloned()
.unwrap_or_default();
if let Err(error) = <Pallet<T>>::delegate_with_auto_compound(
T::RuntimeOrigin::from(Some(delegator.clone()).into()),
target.clone(),
balance,
auto_compound,
cd_count,
cd_auto_compound_count,
dd_count,
) {
log::warn!("Delegate failed in genesis with error {:?}", error);
} else {
if let Some(x) = col_delegator_count.get_mut(target) {
*x = x.saturating_add(1u32);
} else {
col_delegator_count.insert(target.clone(), 1u32);
};
if let Some(x) = del_delegation_count.get_mut(delegator) {
*x = x.saturating_add(1u32);
} else {
del_delegation_count.insert(delegator.clone(), 1u32);
};
if !auto_compound.is_zero() {
col_auto_compound_delegator_count
.entry(target.clone())
.and_modify(|x| *x = x.saturating_add(1))
.or_insert(1);
}
}
}
<CollatorCommission<T>>::put(self.collator_commission);
let pbr = InflationDistributionAccount {
account: T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
.expect("infinite length input; no invalid inputs for type; qed"),
percent: self.parachain_bond_reserve_percent,
};
let zeroed_account = InflationDistributionAccount {
account: T::AccountId::decode(&mut sp_runtime::traits::TrailingZeroInput::zeroes())
.expect("infinite length input; no invalid inputs for type; qed"),
percent: Percent::zero(),
};
<InflationDistributionInfo<T>>::put::<InflationDistributionConfig<T::AccountId>>(
[pbr, zeroed_account].into(),
);
assert!(
self.num_selected_candidates >= T::MinSelectedCandidates::get(),
"{:?}",
Error::<T>::CannotSetBelowMin
);
assert!(
self.num_selected_candidates <= T::MaxCandidates::get(),
"{:?}",
Error::<T>::CannotSetAboveMaxCandidates
);
<TotalSelected<T>>::put(self.num_selected_candidates);
let (_, v_count, _, total_staked) = <Pallet<T>>::select_top_candidates(1u32);
let round: RoundInfo<BlockNumberFor<T>> =
RoundInfo::new(1u32, Zero::zero(), self.blocks_per_round, 0);
<Round<T>>::put(round);
<Pallet<T>>::deposit_event(Event::NewRound {
starting_block: Zero::zero(),
round: 1u32,
selected_collators_number: v_count,
total_balance: total_staked,
});
}
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
pub fn set_staking_expectations(
origin: OriginFor<T>,
expectations: Range<BalanceOf<T>>,
) -> DispatchResultWithPostInfo {
T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
ensure!(expectations.is_valid(), Error::<T>::InvalidSchedule);
let mut config = <InflationConfig<T>>::get();
ensure!(
config.expect != expectations,
Error::<T>::NoWritingSameValue
);
config.set_expectations(expectations);
Self::deposit_event(Event::StakeExpectationsSet {
expect_min: config.expect.min,
expect_ideal: config.expect.ideal,
expect_max: config.expect.max,
});
<InflationConfig<T>>::put(config);
Ok(().into())
}
#[pallet::call_index(1)]
#[pallet::weight(<T as Config>::WeightInfo::set_inflation())]
pub fn set_inflation(
origin: OriginFor<T>,
schedule: Range<Perbill>,
) -> DispatchResultWithPostInfo {
T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
ensure!(schedule.is_valid(), Error::<T>::InvalidSchedule);
let mut config = <InflationConfig<T>>::get();
ensure!(config.annual != schedule, Error::<T>::NoWritingSameValue);
config.annual = schedule;
config.set_round_from_annual::<T>(schedule);
Self::deposit_event(Event::InflationSet {
annual_min: config.annual.min,
annual_ideal: config.annual.ideal,
annual_max: config.annual.max,
round_min: config.round.min,
round_ideal: config.round.ideal,
round_max: config.round.max,
});
<InflationConfig<T>>::put(config);
Ok(().into())
}
#[pallet::call_index(2)]
#[pallet::weight(<T as Config>::WeightInfo::set_parachain_bond_account())]
pub fn set_parachain_bond_account(
origin: OriginFor<T>,
new: T::AccountId,
) -> DispatchResultWithPostInfo {
T::MonetaryGovernanceOrigin::ensure_origin(origin.clone())?;
let old = <InflationDistributionInfo<T>>::get().0;
let new = InflationDistributionAccount {
account: new,
percent: old[0].percent.clone(),
};
Pallet::<T>::set_inflation_distribution_config(origin, [new, old[1].clone()].into())
}
#[pallet::call_index(3)]
#[pallet::weight(<T as Config>::WeightInfo::set_parachain_bond_reserve_percent())]
pub fn set_parachain_bond_reserve_percent(
origin: OriginFor<T>,
new: Percent,
) -> DispatchResultWithPostInfo {
T::MonetaryGovernanceOrigin::ensure_origin(origin.clone())?;
let old = <InflationDistributionInfo<T>>::get().0;
let new = InflationDistributionAccount {
account: old[0].account.clone(),
percent: new,
};
Pallet::<T>::set_inflation_distribution_config(origin, [new, old[1].clone()].into())
}
#[pallet::call_index(4)]
#[pallet::weight(<T as Config>::WeightInfo::set_total_selected())]
pub fn set_total_selected(origin: OriginFor<T>, new: u32) -> DispatchResultWithPostInfo {
frame_system::ensure_root(origin)?;
ensure!(
new >= T::MinSelectedCandidates::get(),
Error::<T>::CannotSetBelowMin
);
ensure!(
new <= T::MaxCandidates::get(),
Error::<T>::CannotSetAboveMaxCandidates
);
let old = <TotalSelected<T>>::get();
ensure!(old != new, Error::<T>::NoWritingSameValue);
ensure!(
new < <Round<T>>::get().length,
Error::<T>::RoundLengthMustBeGreaterThanTotalSelectedCollators,
);
<TotalSelected<T>>::put(new);
Self::deposit_event(Event::TotalSelectedSet { old, new });
Ok(().into())
}
#[pallet::call_index(5)]
#[pallet::weight(<T as Config>::WeightInfo::set_collator_commission())]
pub fn set_collator_commission(
origin: OriginFor<T>,
new: Perbill,
) -> DispatchResultWithPostInfo {
frame_system::ensure_root(origin)?;
let old = <CollatorCommission<T>>::get();
ensure!(old != new, Error::<T>::NoWritingSameValue);
<CollatorCommission<T>>::put(new);
Self::deposit_event(Event::CollatorCommissionSet { old, new });
Ok(().into())
}
#[pallet::call_index(6)]
#[pallet::weight(<T as Config>::WeightInfo::set_blocks_per_round())]
pub fn set_blocks_per_round(origin: OriginFor<T>, new: u32) -> DispatchResultWithPostInfo {
frame_system::ensure_root(origin)?;
ensure!(
new >= T::MinBlocksPerRound::get(),
Error::<T>::CannotSetBelowMin
);
let mut round = <Round<T>>::get();
let (now, first, old) = (round.current, round.first, round.length);
ensure!(old != new, Error::<T>::NoWritingSameValue);
ensure!(
new > <TotalSelected<T>>::get(),
Error::<T>::RoundLengthMustBeGreaterThanTotalSelectedCollators,
);
round.length = new;
let mut inflation_config = <InflationConfig<T>>::get();
inflation_config.reset_round::<T>(new);
<Round<T>>::put(round);
Self::deposit_event(Event::BlocksPerRoundSet {
current_round: now,
first_block: first,
old: old,
new: new,
new_per_round_inflation_min: inflation_config.round.min,
new_per_round_inflation_ideal: inflation_config.round.ideal,
new_per_round_inflation_max: inflation_config.round.max,
});
<InflationConfig<T>>::put(inflation_config);
Ok(().into())
}
#[pallet::call_index(7)]
#[pallet::weight(<T as Config>::WeightInfo::join_candidates(*candidate_count))]
pub fn join_candidates(
origin: OriginFor<T>,
bond: BalanceOf<T>,
candidate_count: u32,
) -> DispatchResultWithPostInfo {
let acc = ensure_signed(origin)?;
ensure!(
bond >= T::MinCandidateStk::get(),
Error::<T>::CandidateBondBelowMin
);
Self::join_candidates_inner(acc, bond, candidate_count)
}
#[pallet::call_index(8)]
#[pallet::weight(<T as Config>::WeightInfo::schedule_leave_candidates(*candidate_count))]
pub fn schedule_leave_candidates(
origin: OriginFor<T>,
candidate_count: u32,
) -> DispatchResultWithPostInfo {
let collator = ensure_signed(origin)?;
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
let (now, when) = state.schedule_leave::<T>()?;
let mut candidates = <CandidatePool<T>>::get();
ensure!(
candidate_count >= candidates.0.len() as u32,
Error::<T>::TooLowCandidateCountToLeaveCandidates
);
if candidates.remove(&Bond::from_owner(collator.clone())) {
<CandidatePool<T>>::put(candidates);
}
<CandidateInfo<T>>::insert(&collator, state);
Self::deposit_event(Event::CandidateScheduledExit {
exit_allowed_round: now,
candidate: collator,
scheduled_exit: when,
});
Ok(().into())
}
#[pallet::call_index(9)]
#[pallet::weight(
<T as Config>::WeightInfo::execute_leave_candidates_worst_case(*candidate_delegation_count)
)]
pub fn execute_leave_candidates(
origin: OriginFor<T>,
candidate: T::AccountId,
candidate_delegation_count: u32,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?;
let state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
ensure!(
state.delegation_count <= candidate_delegation_count,
Error::<T>::TooLowCandidateDelegationCountToLeaveCandidates
);
<Pallet<T>>::execute_leave_candidates_inner(candidate)
}
#[pallet::call_index(10)]
#[pallet::weight(<T as Config>::WeightInfo::cancel_leave_candidates(*candidate_count))]
pub fn cancel_leave_candidates(
origin: OriginFor<T>,
candidate_count: u32,
) -> DispatchResultWithPostInfo {
let collator = ensure_signed(origin)?;
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
ensure!(state.is_leaving(), Error::<T>::CandidateNotLeaving);
state.go_online();
let mut candidates = <CandidatePool<T>>::get();
ensure!(
candidates.0.len() as u32 <= candidate_count,
Error::<T>::TooLowCandidateCountWeightHintCancelLeaveCandidates
);
let maybe_inserted_candidate = candidates
.try_insert(Bond {
owner: collator.clone(),
amount: state.total_counted,
})
.map_err(|_| Error::<T>::CandidateLimitReached)?;
ensure!(maybe_inserted_candidate, Error::<T>::AlreadyActive);
<CandidatePool<T>>::put(candidates);
<CandidateInfo<T>>::insert(&collator, state);
Self::deposit_event(Event::CancelledCandidateExit {
candidate: collator,
});
Ok(().into())
}
#[pallet::call_index(11)]
#[pallet::weight(<T as Config>::WeightInfo::go_offline(MAX_CANDIDATES))]
pub fn go_offline(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let collator = ensure_signed(origin)?;
<Pallet<T>>::go_offline_inner(collator)
}
#[pallet::call_index(12)]
#[pallet::weight(<T as Config>::WeightInfo::go_online(MAX_CANDIDATES))]
pub fn go_online(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let collator = ensure_signed(origin)?;
<Pallet<T>>::go_online_inner(collator)
}
#[pallet::call_index(13)]
#[pallet::weight(<T as Config>::WeightInfo::candidate_bond_more(MAX_CANDIDATES))]
pub fn candidate_bond_more(
origin: OriginFor<T>,
more: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let candidate = ensure_signed(origin)?;
<Pallet<T>>::candidate_bond_more_inner(candidate, more)
}
#[pallet::call_index(14)]
#[pallet::weight(<T as Config>::WeightInfo::schedule_candidate_bond_less())]
pub fn schedule_candidate_bond_less(
origin: OriginFor<T>,
less: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let collator = ensure_signed(origin)?;
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
let when = state.schedule_bond_less::<T>(less)?;
<CandidateInfo<T>>::insert(&collator, state);
Self::deposit_event(Event::CandidateBondLessRequested {
candidate: collator,
amount_to_decrease: less,
execute_round: when,
});
Ok(().into())
}
#[pallet::call_index(15)]
#[pallet::weight(<T as Config>::WeightInfo::execute_candidate_bond_less(MAX_CANDIDATES))]
pub fn execute_candidate_bond_less(
origin: OriginFor<T>,
candidate: T::AccountId,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?; <Pallet<T>>::execute_candidate_bond_less_inner(candidate)
}
#[pallet::call_index(16)]
#[pallet::weight(<T as Config>::WeightInfo::cancel_candidate_bond_less())]
pub fn cancel_candidate_bond_less(origin: OriginFor<T>) -> DispatchResultWithPostInfo {
let collator = ensure_signed(origin)?;
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
state.cancel_bond_less::<T>(collator.clone())?;
<CandidateInfo<T>>::insert(&collator, state);
Ok(().into())
}
#[pallet::call_index(17)]
#[pallet::weight(
<T as Config>::WeightInfo::delegate_with_auto_compound_worst()
)]
pub fn delegate(
origin: OriginFor<T>,
candidate: T::AccountId,
amount: BalanceOf<T>,
candidate_delegation_count: u32,
delegation_count: u32,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
<AutoCompoundDelegations<T>>::delegate_with_auto_compound(
candidate,
delegator,
amount,
Percent::zero(),
candidate_delegation_count,
0,
delegation_count,
)
}
#[pallet::call_index(18)]
#[pallet::weight(
<T as Config>::WeightInfo::delegate_with_auto_compound(
*candidate_delegation_count,
*candidate_auto_compounding_delegation_count,
*delegation_count,
)
)]
pub fn delegate_with_auto_compound(
origin: OriginFor<T>,
candidate: T::AccountId,
amount: BalanceOf<T>,
auto_compound: Percent,
candidate_delegation_count: u32,
candidate_auto_compounding_delegation_count: u32,
delegation_count: u32,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
<AutoCompoundDelegations<T>>::delegate_with_auto_compound(
candidate,
delegator,
amount,
auto_compound,
candidate_delegation_count,
candidate_auto_compounding_delegation_count,
delegation_count,
)
}
#[pallet::call_index(19)]
#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
pub fn removed_call_19(_origin: OriginFor<T>) -> DispatchResultWithPostInfo {
fail!(Error::<T>::RemovedCall)
}
#[pallet::call_index(20)]
#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
pub fn removed_call_20(_origin: OriginFor<T>) -> DispatchResultWithPostInfo {
fail!(Error::<T>::RemovedCall)
}
#[pallet::call_index(21)]
#[pallet::weight(<T as Config>::WeightInfo::set_staking_expectations())]
pub fn removed_call_21(_origin: OriginFor<T>) -> DispatchResultWithPostInfo {
fail!(Error::<T>::RemovedCall)
}
#[pallet::call_index(22)]
#[pallet::weight(<T as Config>::WeightInfo::schedule_revoke_delegation(
T::MaxTopDelegationsPerCandidate::get() + T::MaxBottomDelegationsPerCandidate::get()
))]
pub fn schedule_revoke_delegation(
origin: OriginFor<T>,
collator: T::AccountId,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
Self::delegation_schedule_revoke(collator, delegator)
}
#[pallet::call_index(23)]
#[pallet::weight(<T as Config>::WeightInfo::delegator_bond_more(
T::MaxTopDelegationsPerCandidate::get() + T::MaxBottomDelegationsPerCandidate::get()
))]
pub fn delegator_bond_more(
origin: OriginFor<T>,
candidate: T::AccountId,
more: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
let (in_top, weight) = Self::delegation_bond_more_without_event(
delegator.clone(),
candidate.clone(),
more.clone(),
)?;
Pallet::<T>::deposit_event(Event::DelegationIncreased {
delegator,
candidate,
amount: more,
in_top,
});
Ok(Some(weight).into())
}
#[pallet::call_index(24)]
#[pallet::weight(<T as Config>::WeightInfo::schedule_delegator_bond_less(
T::MaxTopDelegationsPerCandidate::get() + T::MaxBottomDelegationsPerCandidate::get()
))]
pub fn schedule_delegator_bond_less(
origin: OriginFor<T>,
candidate: T::AccountId,
less: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
Self::delegation_schedule_bond_decrease(candidate, delegator, less)
}
#[pallet::call_index(25)]
#[pallet::weight(<T as Config>::WeightInfo::execute_delegator_revoke_delegation_worst())]
pub fn execute_delegation_request(
origin: OriginFor<T>,
delegator: T::AccountId,
candidate: T::AccountId,
) -> DispatchResultWithPostInfo {
ensure_signed(origin)?; Self::delegation_execute_scheduled_request(candidate, delegator)
}
#[pallet::call_index(26)]
#[pallet::weight(<T as Config>::WeightInfo::cancel_delegation_request(350))]
pub fn cancel_delegation_request(
origin: OriginFor<T>,
candidate: T::AccountId,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
Self::delegation_cancel_request(candidate, delegator)
}
#[pallet::call_index(27)]
#[pallet::weight(<T as Config>::WeightInfo::set_auto_compound(
*candidate_auto_compounding_delegation_count_hint,
*delegation_count_hint,
))]
pub fn set_auto_compound(
origin: OriginFor<T>,
candidate: T::AccountId,
value: Percent,
candidate_auto_compounding_delegation_count_hint: u32,
delegation_count_hint: u32,
) -> DispatchResultWithPostInfo {
let delegator = ensure_signed(origin)?;
<AutoCompoundDelegations<T>>::set_auto_compound(
candidate,
delegator,
value,
candidate_auto_compounding_delegation_count_hint,
delegation_count_hint,
)
}
#[pallet::call_index(28)]
#[pallet::weight(
T::DbWeight::get().reads_writes(2 * candidates.len() as u64, candidates.len() as u64)
)]
pub fn hotfix_remove_delegation_requests_exited_candidates(
origin: OriginFor<T>,
candidates: Vec<T::AccountId>,
) -> DispatchResult {
ensure_signed(origin)?;
ensure!(candidates.len() < 100, <Error<T>>::InsufficientBalance);
for candidate in &candidates {
ensure!(
<CandidateInfo<T>>::get(&candidate).is_none(),
<Error<T>>::CandidateNotLeaving
);
ensure!(
<DelegationScheduledRequests<T>>::get(&candidate).is_empty(),
<Error<T>>::CandidateNotLeaving
);
}
for candidate in candidates {
<DelegationScheduledRequests<T>>::remove(candidate);
}
Ok(().into())
}
#[pallet::call_index(29)]
#[pallet::weight(<T as Config>::WeightInfo::notify_inactive_collator())]
pub fn notify_inactive_collator(
origin: OriginFor<T>,
collator: T::AccountId,
) -> DispatchResult {
ensure!(
<EnableMarkingOffline<T>>::get(),
<Error<T>>::MarkingOfflineNotEnabled
);
ensure_signed(origin)?;
let mut collators_len = 0usize;
let max_collators = <TotalSelected<T>>::get();
if let Some(len) = <SelectedCandidates<T>>::decode_len() {
collators_len = len;
};
ensure!(
collators_len * 3 > (max_collators * 2) as usize,
<Error<T>>::TooLowCollatorCountToNotifyAsInactive
);
let round_info = <Round<T>>::get();
let max_offline_rounds = T::MaxOfflineRounds::get();
ensure!(
round_info.current > max_offline_rounds,
<Error<T>>::CurrentRoundTooLow
);
let first_round_to_check = round_info.current.saturating_sub(max_offline_rounds);
let rounds_to_check = first_round_to_check..round_info.current;
let mut inactive_counter: RoundIndex = 0u32;
for r in rounds_to_check {
let stake = <AtStake<T>>::get(r, &collator);
let pts = <AwardedPts<T>>::get(r, &collator);
if stake.is_some() && pts.is_zero() {
inactive_counter = inactive_counter.saturating_add(1);
}
}
if inactive_counter == max_offline_rounds {
let _ = T::OnInactiveCollator::on_inactive_collator(
collator.clone(),
round_info.current.saturating_sub(1),
);
} else {
return Err(<Error<T>>::CannotBeNotifiedAsInactive.into());
}
Ok(().into())
}
#[pallet::call_index(30)]
#[pallet::weight(
Weight::from_parts(3_000_000u64, 4_000u64)
.saturating_add(T::DbWeight::get().writes(1u64))
)]
pub fn enable_marking_offline(origin: OriginFor<T>, value: bool) -> DispatchResult {
ensure_root(origin)?;
<EnableMarkingOffline<T>>::set(value);
Ok(())
}
#[pallet::call_index(31)]
#[pallet::weight(<T as Config>::WeightInfo::join_candidates(*candidate_count))]
pub fn force_join_candidates(
origin: OriginFor<T>,
account: T::AccountId,
bond: BalanceOf<T>,
candidate_count: u32,
) -> DispatchResultWithPostInfo {
T::MonetaryGovernanceOrigin::ensure_origin(origin.clone())?;
Self::join_candidates_inner(account, bond, candidate_count)
}
#[pallet::call_index(32)]
#[pallet::weight(<T as Config>::WeightInfo::set_inflation_distribution_config())]
pub fn set_inflation_distribution_config(
origin: OriginFor<T>,
new: InflationDistributionConfig<T::AccountId>,
) -> DispatchResultWithPostInfo {
T::MonetaryGovernanceOrigin::ensure_origin(origin)?;
let old = <InflationDistributionInfo<T>>::get().0;
let new = new.0;
ensure!(old != new, Error::<T>::NoWritingSameValue);
let total_percent = new.iter().fold(0, |acc, x| acc + x.percent.deconstruct());
ensure!(
total_percent <= 100,
Error::<T>::TotalInflationDistributionPercentExceeds100,
);
<InflationDistributionInfo<T>>::put::<InflationDistributionConfig<T::AccountId>>(
new.clone().into(),
);
Self::deposit_event(Event::InflationDistributionConfigUpdated {
old: old.into(),
new: new.into(),
});
Ok(().into())
}
}
pub(crate) enum RewardPayment {
Paid,
Skipped,
Finished,
}
impl<T: Config> Pallet<T> {
pub fn set_candidate_bond_to_zero(acc: &T::AccountId) -> Weight {
let actual_weight =
<T as Config>::WeightInfo::set_candidate_bond_to_zero(T::MaxCandidates::get());
if let Some(mut state) = <CandidateInfo<T>>::get(&acc) {
state.bond_less::<T>(acc.clone(), state.bond);
<CandidateInfo<T>>::insert(&acc, state);
}
actual_weight
}
pub fn is_delegator(acc: &T::AccountId) -> bool {
<DelegatorState<T>>::get(acc).is_some()
}
pub fn is_candidate(acc: &T::AccountId) -> bool {
<CandidateInfo<T>>::get(acc).is_some()
}
pub fn is_selected_candidate(acc: &T::AccountId) -> bool {
<SelectedCandidates<T>>::get().binary_search(acc).is_ok()
}
pub fn join_candidates_inner(
acc: T::AccountId,
bond: BalanceOf<T>,
candidate_count: u32,
) -> DispatchResultWithPostInfo {
ensure!(!Self::is_candidate(&acc), Error::<T>::CandidateExists);
ensure!(!Self::is_delegator(&acc), Error::<T>::DelegatorExists);
let mut candidates = <CandidatePool<T>>::get();
let old_count = candidates.0.len() as u32;
ensure!(
candidate_count >= old_count,
Error::<T>::TooLowCandidateCountWeightHintJoinCandidates
);
let maybe_inserted_candidate = candidates
.try_insert(Bond {
owner: acc.clone(),
amount: bond,
})
.map_err(|_| Error::<T>::CandidateLimitReached)?;
ensure!(maybe_inserted_candidate, Error::<T>::CandidateExists);
ensure!(
Self::get_collator_stakable_free_balance(&acc) >= bond,
Error::<T>::InsufficientBalance,
);
T::Currency::set_lock(COLLATOR_LOCK_ID, &acc, bond, WithdrawReasons::all());
let candidate = CandidateMetadata::new(bond);
<CandidateInfo<T>>::insert(&acc, candidate);
let empty_delegations: Delegations<T::AccountId, BalanceOf<T>> = Default::default();
<TopDelegations<T>>::insert(&acc, empty_delegations.clone());
<BottomDelegations<T>>::insert(&acc, empty_delegations);
<CandidatePool<T>>::put(candidates);
let new_total = <Total<T>>::get().saturating_add(bond);
<Total<T>>::put(new_total);
Self::deposit_event(Event::JoinedCollatorCandidates {
account: acc,
amount_locked: bond,
new_total_amt_locked: new_total,
});
Ok(().into())
}
pub fn go_offline_inner(collator: T::AccountId) -> DispatchResultWithPostInfo {
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
let mut candidates = <CandidatePool<T>>::get();
let actual_weight = <T as Config>::WeightInfo::go_offline(candidates.0.len() as u32);
ensure!(
state.is_active(),
DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: <Error<T>>::AlreadyOffline.into(),
}
);
state.go_offline();
if candidates.remove(&Bond::from_owner(collator.clone())) {
<CandidatePool<T>>::put(candidates);
}
<CandidateInfo<T>>::insert(&collator, state);
Self::deposit_event(Event::CandidateWentOffline {
candidate: collator,
});
Ok(Some(actual_weight).into())
}
pub fn go_online_inner(collator: T::AccountId) -> DispatchResultWithPostInfo {
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
let mut candidates = <CandidatePool<T>>::get();
let actual_weight = <T as Config>::WeightInfo::go_online(candidates.0.len() as u32);
ensure!(
!state.is_active(),
DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: <Error<T>>::AlreadyActive.into(),
}
);
ensure!(
!state.is_leaving(),
DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: <Error<T>>::CannotGoOnlineIfLeaving.into(),
}
);
state.go_online();
let maybe_inserted_candidate = candidates
.try_insert(Bond {
owner: collator.clone(),
amount: state.total_counted,
})
.map_err(|_| Error::<T>::CandidateLimitReached)?;
ensure!(
maybe_inserted_candidate,
DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: <Error<T>>::AlreadyActive.into(),
},
);
<CandidatePool<T>>::put(candidates);
<CandidateInfo<T>>::insert(&collator, state);
Self::deposit_event(Event::CandidateBackOnline {
candidate: collator,
});
Ok(Some(actual_weight).into())
}
pub fn candidate_bond_more_inner(
collator: T::AccountId,
more: BalanceOf<T>,
) -> DispatchResultWithPostInfo {
let mut state = <CandidateInfo<T>>::get(&collator).ok_or(Error::<T>::CandidateDNE)?;
let actual_weight =
<T as Config>::WeightInfo::candidate_bond_more(T::MaxCandidates::get());
state
.bond_more::<T>(collator.clone(), more)
.map_err(|err| DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: err,
})?;
let (is_active, total_counted) = (state.is_active(), state.total_counted);
<CandidateInfo<T>>::insert(&collator, state);
if is_active {
Self::update_active(collator, total_counted);
}
Ok(Some(actual_weight).into())
}
pub fn execute_candidate_bond_less_inner(
candidate: T::AccountId,
) -> DispatchResultWithPostInfo {
let mut state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
let actual_weight =
<T as Config>::WeightInfo::execute_candidate_bond_less(T::MaxCandidates::get());
state
.execute_bond_less::<T>(candidate.clone())
.map_err(|err| DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: err,
})?;
<CandidateInfo<T>>::insert(&candidate, state);
Ok(Some(actual_weight).into())
}
pub fn execute_leave_candidates_inner(
candidate: T::AccountId,
) -> DispatchResultWithPostInfo {
let state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
let actual_auto_compound_delegation_count =
<AutoCompoundingDelegations<T>>::decode_len(&candidate).unwrap_or_default() as u32;
let actual_delegation_count = state.delegation_count;
let actual_weight = <T as Config>::WeightInfo::execute_leave_candidates_ideal(
actual_delegation_count,
actual_auto_compound_delegation_count,
);
state
.can_leave::<T>()
.map_err(|err| DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: err,
})?;
let return_stake = |bond: Bond<T::AccountId, BalanceOf<T>>| {
let mut delegator = DelegatorState::<T>::get(&bond.owner).expect(
"Collator state and delegator state are consistent.
Collator state has a record of this delegation. Therefore,
Delegator state also has a record. qed.",
);
if let Some(remaining) = delegator.rm_delegation::<T>(&candidate) {
Self::delegation_remove_request_with_state(
&candidate,
&bond.owner,
&mut delegator,
);
<AutoCompoundDelegations<T>>::remove_auto_compound(&candidate, &bond.owner);
if remaining.is_zero() {
<DelegatorState<T>>::remove(&bond.owner);
T::Currency::remove_lock(DELEGATOR_LOCK_ID, &bond.owner);
} else {
<DelegatorState<T>>::insert(&bond.owner, delegator);
}
} else {
T::Currency::remove_lock(DELEGATOR_LOCK_ID, &bond.owner);
}
};
let mut total_backing = state.bond;
let top_delegations =
<TopDelegations<T>>::take(&candidate).expect("CandidateInfo existence checked");
for bond in top_delegations.delegations {
return_stake(bond);
}
total_backing = total_backing.saturating_add(top_delegations.total);
let bottom_delegations =
<BottomDelegations<T>>::take(&candidate).expect("CandidateInfo existence checked");
for bond in bottom_delegations.delegations {
return_stake(bond);
}
total_backing = total_backing.saturating_add(bottom_delegations.total);
T::Currency::remove_lock(COLLATOR_LOCK_ID, &candidate);
<CandidateInfo<T>>::remove(&candidate);
<DelegationScheduledRequests<T>>::remove(&candidate);
<AutoCompoundingDelegations<T>>::remove(&candidate);
<TopDelegations<T>>::remove(&candidate);
<BottomDelegations<T>>::remove(&candidate);
let new_total_staked = <Total<T>>::get().saturating_sub(total_backing);
<Total<T>>::put(new_total_staked);
Self::deposit_event(Event::CandidateLeft {
ex_candidate: candidate,
unlocked_amount: total_backing,
new_total_amt_locked: new_total_staked,
});
Ok(Some(actual_weight).into())
}
pub fn get_delegator_stakable_balance(acc: &T::AccountId) -> BalanceOf<T> {
let mut stakable_balance =
T::Currency::free_balance(acc).saturating_add(T::Currency::reserved_balance(acc));
if let Some(state) = <DelegatorState<T>>::get(acc) {
stakable_balance = stakable_balance.saturating_sub(state.total());
}
stakable_balance
}
pub fn get_collator_stakable_free_balance(acc: &T::AccountId) -> BalanceOf<T> {
let mut balance = T::Currency::free_balance(acc);
if let Some(info) = <CandidateInfo<T>>::get(acc) {
balance = balance.saturating_sub(info.bond);
}
balance
}
pub fn delegation_auto_compound(
candidate: &T::AccountId,
delegator: &T::AccountId,
) -> Percent {
<AutoCompoundDelegations<T>>::auto_compound(candidate, delegator)
}
pub(crate) fn update_active(candidate: T::AccountId, total: BalanceOf<T>) {
let mut candidates = <CandidatePool<T>>::get();
candidates.remove(&Bond::from_owner(candidate.clone()));
candidates
.try_insert(Bond {
owner: candidate,
amount: total,
})
.expect(
"the candidate is removed in previous step so the length cannot increase; qed",
);
<CandidatePool<T>>::put(candidates);
}
fn compute_issuance(round_duration: u64, round_length: u32) -> BalanceOf<T> {
let ideal_duration: BalanceOf<T> = round_length
.saturating_mul(T::BlockTime::get() as u32)
.into();
let config = <InflationConfig<T>>::get();
let round_issuance = crate::inflation::round_issuance_range::<T>(config.round);
BalanceOf::<T>::from(round_duration as u32).saturating_mul(round_issuance.ideal)
/ (ideal_duration)
}
pub(crate) fn delegator_leaves_candidate(
candidate: T::AccountId,
delegator: T::AccountId,
amount: BalanceOf<T>,
) -> DispatchResult {
let mut state = <CandidateInfo<T>>::get(&candidate).ok_or(Error::<T>::CandidateDNE)?;
state.rm_delegation_if_exists::<T>(&candidate, delegator.clone(), amount)?;
let new_total_locked = <Total<T>>::get().saturating_sub(amount);
<Total<T>>::put(new_total_locked);
let new_total = state.total_counted;
<CandidateInfo<T>>::insert(&candidate, state);
Self::deposit_event(Event::DelegatorLeftCandidate {
delegator: delegator,
candidate: candidate,
unstaked_amount: amount,
total_candidate_staked: new_total,
});
Ok(())
}
pub(crate) fn prepare_staking_payouts(
round_info: RoundInfo<BlockNumberFor<T>>,
round_duration: u64,
) -> Weight {
let RoundInfo {
current: now,
length: round_length,
..
} = round_info;
let prepare_payout_for_round = now - 1;
if <Points<T>>::get(prepare_payout_for_round).is_zero() {
return Weight::zero();
}
let total_issuance = Self::compute_issuance(round_duration, round_length);
let mut left_issuance = total_issuance;
let configs = <InflationDistributionInfo<T>>::get().0;
for (index, config) in configs.iter().enumerate() {
if config.percent.is_zero() {
continue;
}
let reserve = config.percent * total_issuance;
if let Ok(imb) = T::Currency::deposit_into_existing(&config.account, reserve) {
left_issuance = left_issuance.saturating_sub(imb.peek());
Self::deposit_event(Event::InflationDistributed {
index: index as u32,
account: config.account.clone(),
value: imb.peek(),
});
}
}
let payout = DelayedPayout {
round_issuance: total_issuance,
total_staking_reward: left_issuance,
collator_commission: <CollatorCommission<T>>::get(),
};
<DelayedPayouts<T>>::insert(prepare_payout_for_round, payout);
<T as Config>::WeightInfo::prepare_staking_payouts()
}
fn handle_delayed_payouts(now: RoundIndex) -> Weight {
let delay = T::RewardPaymentDelay::get();
if now < delay {
return Weight::from_parts(0u64, 0);
}
let paid_for_round = now.saturating_sub(delay);
if let Some(payout_info) = <DelayedPayouts<T>>::get(paid_for_round) {
let result = Self::pay_one_collator_reward(paid_for_round, payout_info);
if matches!(result.0, RewardPayment::Finished) {
<DelayedPayouts<T>>::remove(paid_for_round);
<Points<T>>::remove(paid_for_round);
}
result.1 } else {
Weight::from_parts(0u64, 0)
}
}
pub(crate) fn pay_one_collator_reward(
paid_for_round: RoundIndex,
payout_info: DelayedPayout<BalanceOf<T>>,
) -> (RewardPayment, Weight) {
let mut early_weight = Weight::zero();
let total_points = <Points<T>>::get(paid_for_round);
early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 0));
if total_points.is_zero() {
log::warn!("pay_one_collator_reward called with no <Points<T>> for the round!");
return (RewardPayment::Finished, early_weight);
}
let collator_fee = payout_info.collator_commission;
let collator_issuance = collator_fee * payout_info.round_issuance;
if let Some((collator, state)) =
<AtStake<T>>::iter_prefix(paid_for_round).drain().next()
{
early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
let pts = <AwardedPts<T>>::take(paid_for_round, &collator);
early_weight = early_weight.saturating_add(T::DbWeight::get().reads_writes(1, 1));
if pts == 0 {
return (RewardPayment::Skipped, early_weight);
}
let mut extra_weight = Weight::zero();
let pct_due = Perbill::from_rational(pts, total_points);
let total_paid = pct_due * payout_info.total_staking_reward;
let mut amt_due = total_paid;
let num_delegators = state.delegations.len();
let mut num_paid_delegations = 0u32;
let mut num_auto_compounding = 0u32;
let num_scheduled_requests =
<DelegationScheduledRequests<T>>::decode_len(&collator).unwrap_or_default();
if state.delegations.is_empty() {
extra_weight = extra_weight
.saturating_add(T::PayoutCollatorReward::payout_collator_reward(
paid_for_round,
collator.clone(),
amt_due,
))
.saturating_add(T::OnCollatorPayout::on_collator_payout(
paid_for_round,
collator.clone(),
amt_due,
));
} else {
let collator_pct = Perbill::from_rational(state.bond, state.total);
let commission = pct_due * collator_issuance;
amt_due = amt_due.saturating_sub(commission);
let collator_reward = (collator_pct * amt_due).saturating_add(commission);
extra_weight = extra_weight
.saturating_add(T::PayoutCollatorReward::payout_collator_reward(
paid_for_round,
collator.clone(),
collator_reward,
))
.saturating_add(T::OnCollatorPayout::on_collator_payout(
paid_for_round,
collator.clone(),
collator_reward,
));
for BondWithAutoCompound {
owner,
amount,
auto_compound,
} in state.delegations
{
let percent = Perbill::from_rational(amount, state.total);
let due = percent * amt_due;
if !due.is_zero() {
num_auto_compounding += if auto_compound.is_zero() { 0 } else { 1 };
num_paid_delegations += 1u32;
Self::mint_and_compound(
due,
auto_compound.clone(),
collator.clone(),
owner.clone(),
);
}
}
}
extra_weight = extra_weight.saturating_add(
<T as Config>::WeightInfo::pay_one_collator_reward_best(
num_paid_delegations,
num_auto_compounding,
num_scheduled_requests as u32,
),
);
(
RewardPayment::Paid,
<T as Config>::WeightInfo::pay_one_collator_reward(num_delegators as u32)
.saturating_add(extra_weight),
)
} else {
(RewardPayment::Finished, Weight::from_parts(0u64, 0))
}
}
pub fn compute_top_candidates() -> Vec<T::AccountId> {
let top_n = <TotalSelected<T>>::get() as usize;
if top_n == 0 {
return vec![];
}
let candidates = <CandidatePool<T>>::get().0;
if candidates.len() > top_n {
let sorted_candidates = candidates
.try_mutate(|inner| {
inner.select_nth_unstable_by(top_n - 1, |a, b| {
a.amount
.cmp(&b.amount)
.then_with(|| a.owner.cmp(&b.owner))
.reverse()
});
})
.expect("sort cannot increase item count; qed");
let mut collators = sorted_candidates
.into_iter()
.take(top_n)
.map(|x| x.owner)
.collect::<Vec<_>>();
collators.sort();
collators
} else {
candidates.into_iter().map(|x| x.owner).collect::<Vec<_>>()
}
}
pub(crate) fn select_top_candidates(now: RoundIndex) -> (Weight, u32, u32, BalanceOf<T>) {
let (mut collator_count, mut delegation_count, mut total) =
(0u32, 0u32, BalanceOf::<T>::zero());
let collators = Self::compute_top_candidates();
if collators.is_empty() {
let last_round = now.saturating_sub(1u32);
let mut total_per_candidate: BTreeMap<T::AccountId, BalanceOf<T>> = BTreeMap::new();
for (account, snapshot) in <AtStake<T>>::iter_prefix(last_round) {
collator_count = collator_count.saturating_add(1u32);
delegation_count =
delegation_count.saturating_add(snapshot.delegations.len() as u32);
total = total.saturating_add(snapshot.total);
total_per_candidate.insert(account.clone(), snapshot.total);
<AtStake<T>>::insert(now, account, snapshot);
}
for candidate in <SelectedCandidates<T>>::get() {
let snapshot_total = total_per_candidate
.get(&candidate)
.expect("all selected candidates have snapshots");
Self::deposit_event(Event::CollatorChosen {
round: now,
collator_account: candidate,
total_exposed_amount: *snapshot_total,
})
}
let weight = <T as Config>::WeightInfo::select_top_candidates(0, 0);
return (weight, collator_count, delegation_count, total);
}
for account in collators.iter() {
let state = <CandidateInfo<T>>::get(account)
.expect("all members of CandidateQ must be candidates");
collator_count = collator_count.saturating_add(1u32);
delegation_count = delegation_count.saturating_add(state.delegation_count);
total = total.saturating_add(state.total_counted);
let CountedDelegations {
uncounted_stake,
rewardable_delegations,
} = Self::get_rewardable_delegators(&account);
let total_counted = state.total_counted.saturating_sub(uncounted_stake);
let auto_compounding_delegations = <AutoCompoundingDelegations<T>>::get(&account)
.into_iter()
.map(|x| (x.delegator, x.value))
.collect::<BTreeMap<_, _>>();
let rewardable_delegations = rewardable_delegations
.into_iter()
.map(|d| BondWithAutoCompound {
owner: d.owner.clone(),
amount: d.amount,
auto_compound: auto_compounding_delegations
.get(&d.owner)
.cloned()
.unwrap_or_else(|| Percent::zero()),
})
.collect();
let snapshot = CollatorSnapshot {
bond: state.bond,
delegations: rewardable_delegations,
total: total_counted,
};
<AtStake<T>>::insert(now, account, snapshot);
Self::deposit_event(Event::CollatorChosen {
round: now,
collator_account: account.clone(),
total_exposed_amount: state.total_counted,
});
}
<SelectedCandidates<T>>::put(
BoundedVec::try_from(collators)
.expect("subset of collators is always less than or equal to max candidates"),
);
let avg_delegator_count = delegation_count.checked_div(collator_count).unwrap_or(0);
let weight = <T as Config>::WeightInfo::select_top_candidates(
collator_count,
avg_delegator_count,
);
(weight, collator_count, delegation_count, total)
}
pub(crate) fn get_rewardable_delegators(collator: &T::AccountId) -> CountedDelegations<T> {
let requests = <DelegationScheduledRequests<T>>::get(collator)
.into_iter()
.map(|x| (x.delegator, x.action))
.collect::<BTreeMap<_, _>>();
let mut uncounted_stake = BalanceOf::<T>::zero();
let rewardable_delegations = <TopDelegations<T>>::get(collator)
.expect("all members of CandidateQ must be candidates")
.delegations
.into_iter()
.map(|mut bond| {
bond.amount = match requests.get(&bond.owner) {
None => bond.amount,
Some(DelegationAction::Revoke(_)) => {
uncounted_stake = uncounted_stake.saturating_add(bond.amount);
BalanceOf::<T>::zero()
}
Some(DelegationAction::Decrease(amount)) => {
uncounted_stake = uncounted_stake.saturating_add(*amount);
bond.amount.saturating_sub(*amount)
}
};
bond
})
.collect();
CountedDelegations {
uncounted_stake,
rewardable_delegations,
}
}
pub fn delegation_bond_more_without_event(
delegator: T::AccountId,
candidate: T::AccountId,
more: BalanceOf<T>,
) -> Result<
(bool, Weight),
DispatchErrorWithPostInfo<frame_support::dispatch::PostDispatchInfo>,
> {
let mut state = <DelegatorState<T>>::get(&delegator).ok_or(Error::<T>::DelegatorDNE)?;
ensure!(
!Self::delegation_request_revoke_exists(&candidate, &delegator),
Error::<T>::PendingDelegationRevoke
);
let actual_weight = <T as Config>::WeightInfo::delegator_bond_more(
<DelegationScheduledRequests<T>>::decode_len(&candidate).unwrap_or_default() as u32,
);
let in_top = state
.increase_delegation::<T>(candidate.clone(), more)
.map_err(|err| DispatchErrorWithPostInfo {
post_info: Some(actual_weight).into(),
error: err,
})?;
Ok((in_top, actual_weight))
}
pub fn mint(amt: BalanceOf<T>, to: T::AccountId) {
if let Ok(amount_transferred) = T::Currency::deposit_into_existing(&to, amt) {
Self::deposit_event(Event::Rewarded {
account: to.clone(),
rewards: amount_transferred.peek(),
});
}
}
pub fn mint_collator_reward(
_paid_for_round: RoundIndex,
collator_id: T::AccountId,
amt: BalanceOf<T>,
) -> Weight {
if let Ok(amount_transferred) = T::Currency::deposit_into_existing(&collator_id, amt) {
Self::deposit_event(Event::Rewarded {
account: collator_id.clone(),
rewards: amount_transferred.peek(),
});
}
<T as Config>::WeightInfo::mint_collator_reward()
}
pub fn mint_and_compound(
amt: BalanceOf<T>,
compound_percent: Percent,
candidate: T::AccountId,
delegator: T::AccountId,
) {
if let Ok(amount_transferred) =
T::Currency::deposit_into_existing(&delegator, amt.clone())
{
Self::deposit_event(Event::Rewarded {
account: delegator.clone(),
rewards: amount_transferred.peek(),
});
let compound_amount = compound_percent.mul_ceil(amount_transferred.peek());
if compound_amount.is_zero() {
return;
}
if let Err(err) = Self::delegation_bond_more_without_event(
delegator.clone(),
candidate.clone(),
compound_amount.clone(),
) {
log::debug!(
"skipped compounding staking reward towards candidate '{:?}' for delegator '{:?}': {:?}",
candidate,
delegator,
err
);
return;
};
Pallet::<T>::deposit_event(Event::Compounded {
delegator,
candidate,
amount: compound_amount.clone(),
});
};
}
}
impl<T: Config> Pallet<T> {
fn award_points_to_block_author() {
let author = T::BlockAuthor::get();
let now = <Round<T>>::get().current;
let score_plus_20 = <AwardedPts<T>>::get(now, &author).saturating_add(20);
<AwardedPts<T>>::insert(now, author, score_plus_20);
<Points<T>>::mutate(now, |x| *x = x.saturating_add(20));
}
}
impl<T: Config> nimbus_primitives::CanAuthor<T::AccountId> for Pallet<T> {
fn can_author(account: &T::AccountId, _slot: &u32) -> bool {
Self::is_selected_candidate(account)
}
}
impl<T: Config> Get<Vec<T::AccountId>> for Pallet<T> {
fn get() -> Vec<T::AccountId> {
Self::selected_candidates().into_inner()
}
}
}