#![cfg_attr(not(feature = "std"), no_std)]
use codec::{Decode, Encode, HasCompact};
use frame_support::{
decl_error, decl_event, decl_module, decl_storage,
dispatch::DispatchResult,
ensure,
traits::{
Currency, Get, Imbalance, LockIdentifier, LockableCurrency, OnUnbalanced, Time,
WithdrawReasons,
},
weights::Weight,
IterableStorageDoubleMap, StorageMap, StorageValue,
};
use frame_system::{self as system, ensure_signed};
use pallet_contract_operator::ContractFinder;
use pallet_plasm_rewards::{
traits::{ComputeEraWithParam, EraFinder, ForDappsEraRewardFinder, HistoryDepthFinder},
EraIndex, Releases,
};
pub use pallet_staking::{Forcing, RewardDestination};
use sp_runtime::{
traits::{CheckedSub, Saturating, StaticLookup, Zero},
Perbill, RuntimeDebug,
};
use sp_std::{collections::btree_map::BTreeMap, prelude::*, result, vec::Vec};
#[cfg(test)]
mod mock;
pub mod parameters;
pub mod rewards;
#[cfg(test)]
mod tests;
pub use parameters::StakingParameters;
pub use rewards::ComputeRewardsForDapps;
pub use sp_staking::SessionIndex;
pub type BalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::Balance;
pub type MomentOf<T> = <<T as Trait>::Time as Time>::Moment;
type PositiveImbalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::PositiveImbalance;
type NegativeImbalanceOf<T> =
<<T as Trait>::Currency as Currency<<T as system::Trait>::AccountId>>::NegativeImbalance;
const MAX_NOMINATIONS: usize = 128;
const MAX_UNLOCKING_CHUNKS: usize = 32;
const STAKING_ID: LockIdentifier = *b"dapstake";
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
pub struct Nominations<AccountId, Balance> {
pub targets: Vec<(AccountId, Balance)>,
pub submitted_in: EraIndex,
pub suppressed: bool,
}
#[derive(PartialEq, Encode, Decode, Default, RuntimeDebug)]
pub struct EraStakingPoints<AccountId: Ord, Balance: HasCompact> {
total: Balance,
individual: BTreeMap<AccountId, Balance>,
}
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
pub struct UnlockChunk<Balance: HasCompact> {
#[codec(compact)]
value: Balance,
#[codec(compact)]
era: EraIndex,
}
#[derive(PartialEq, Eq, Clone, Encode, Decode, RuntimeDebug)]
pub struct StakingLedger<AccountId, Balance: HasCompact> {
pub stash: AccountId,
#[codec(compact)]
pub total: Balance,
#[codec(compact)]
pub active: Balance,
pub unlocking: Vec<UnlockChunk<Balance>>,
pub last_reward: Option<EraIndex>,
}
impl<AccountId, Balance: HasCompact + Copy + Saturating> StakingLedger<AccountId, Balance> {
fn consolidate_unlocked(self, current_era: EraIndex) -> Self {
let mut total = self.total;
let unlocking = self
.unlocking
.into_iter()
.filter(|chunk| {
if chunk.era > current_era {
true
} else {
total = total.saturating_sub(chunk.value);
false
}
})
.collect();
Self {
total,
active: self.active,
stash: self.stash,
unlocking,
last_reward: self.last_reward,
}
}
}
pub trait Trait: pallet_session::Trait {
type Currency: LockableCurrency<Self::AccountId, Moment = Self::BlockNumber>;
type ContractFinder: ContractFinder<Self::AccountId, parameters::StakingParameters>;
type BondingDuration: Get<EraIndex>;
type RewardRemainder: OnUnbalanced<NegativeImbalanceOf<Self>>;
type Reward: OnUnbalanced<PositiveImbalanceOf<Self>>;
type Time: Time;
type ComputeRewardsForDapps: ComputeRewardsForDapps;
type EraFinder: EraFinder<EraIndex, SessionIndex, MomentOf<Self>>;
type ForDappsEraReward: ForDappsEraRewardFinder<BalanceOf<Self>>;
type HistoryDepthFinder: HistoryDepthFinder;
type Event: From<Event<Self>> + Into<<Self as system::Trait>::Event>;
}
decl_storage! {
trait Store for Module<T: Trait> as DappsStaking {
pub UntreatedEra get(fn untreated_era): EraIndex;
pub NominatorsUntreatedEra get(fn nominators_untreated_era):
map hasher(twox_64_concat) T::AccountId => EraIndex;
pub OperatorsUntreatedEra get(fn operators_untreated_era):
map hasher(twox_64_concat) T::AccountId => EraIndex;
pub ContractsUntreatedEra get(fn contracts_untreated_era):
map hasher(twox_64_concat) T::AccountId => EraIndex;
pub Bonded get(fn bonded): map hasher(twox_64_concat) T::AccountId => Option<T::AccountId>;
pub Ledger get(fn ledger):
map hasher(blake2_128_concat) T::AccountId
=> Option<StakingLedger<T::AccountId, BalanceOf<T>>>;
pub Payee get(fn payee): map hasher(twox_64_concat) T::AccountId => RewardDestination;
DappsNominations get(fn dapps_nominations): map hasher(twox_64_concat)
T::AccountId => Option<Nominations<T::AccountId, BalanceOf<T>>>;
pub ErasContractsParameters get(fn eras_contracts_parameters):
double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId
=> Option<StakingParameters>;
pub ErasStakingPoints get(fn eras_staking_points):
double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId
=> EraStakingPoints<T::AccountId, BalanceOf<T>>;
pub ErasTotalStake get(fn eras_total_stake):
map hasher(twox_64_concat) EraIndex => BalanceOf<T>;
ErasNominateTotals get(fn eras_nominate_totals):
double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId
=> BalanceOf<T>;
ErasStakedOperators get(fn eras_staked_operators):
double_map hasher(twox_64_concat) EraIndex, hasher(twox_64_concat) T::AccountId
=> BalanceOf<T>;
StorageVersion build(|_: &GenesisConfig| Releases::V1_0_0): Releases;
}
}
decl_event!(
pub enum Event<T>
where
AccountId = <T as system::Trait>::AccountId,
Balance = BalanceOf<T>,
{
Reward(Balance, Balance),
Bonded(AccountId, Balance),
Unbonded(AccountId, Balance),
Withdrawn(AccountId, Balance),
TotalDappsRewards(EraIndex, Balance),
Nominate(AccountId),
}
);
decl_error! {
pub enum Error for Module<T: Trait> {
NotController,
NotStash,
AlreadyBonded,
AlreadyPaired,
EmptyTargets,
DuplicateIndex,
InvalidSlashIndex,
InsufficientValue,
NoMoreChunks,
NoUnlockChunk,
FundedTarget,
InvalidEraToReward,
InvalidNumberOfNominations,
NotSortedAndUnique,
EmptyNominateTargets,
NotOperatedContracts,
NotEnoughStaking,
}
}
decl_module! {
pub struct Module<T: Trait> for enum Call where origin: T::Origin {
fn deposit_event() = default;
fn on_runtime_upgrade() -> Weight {
migrate::<T>();
50_000
}
#[weight = 500_000]
fn bond(origin,
controller: <T::Lookup as StaticLookup>::Source,
#[compact] value: BalanceOf<T>,
payee: RewardDestination
) {
let stash = ensure_signed(origin)?;
if <Bonded<T>>::contains_key(&stash) {
Err("stash already bonded")?
}
let controller = T::Lookup::lookup(controller)?;
if <Ledger<T>>::contains_key(&controller) {
Err("controller already paired")?
}
if value < T::Currency::minimum_balance() {
Err("can not bond with value less than minimum balance")?
}
<Bonded<T>>::insert(&stash, &controller);
<Payee<T>>::insert(&stash, payee);
system::Module::<T>::inc_ref(&stash);
let stash_balance = T::Currency::free_balance(&stash);
let value = value.min(stash_balance);
Self::deposit_event(RawEvent::Bonded(stash.clone(), value.clone()));
let item = StakingLedger {
stash,
total: value,
active: value,
unlocking: vec![],
last_reward: T::EraFinder::current()
};
Self::update_ledger(&controller, &item);
}
#[weight = 500_000]
fn bond_extra(origin, #[compact] max_additional: BalanceOf<T>) {
let stash = ensure_signed(origin)?;
let controller = Self::bonded(&stash).ok_or(Error::<T>::NotStash)?;
let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
let stash_balance = T::Currency::free_balance(&stash);
if let Some(extra) = stash_balance.checked_sub(&ledger.total) {
let extra = extra.min(max_additional);
ledger.total += extra;
ledger.active += extra;
Self::deposit_event(RawEvent::Bonded(stash, extra));
Self::update_ledger(&controller, &ledger);
}
}
#[weight = 400_000]
fn unbond(origin, #[compact] value: BalanceOf<T>) {
let controller = ensure_signed(origin)?;
let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
ensure!(
ledger.unlocking.len() < MAX_UNLOCKING_CHUNKS,
Error::<T>::NoMoreChunks
);
let mut value = value.min(ledger.active);
if !value.is_zero() {
ledger.active -= value;
if ledger.active < T::Currency::minimum_balance() {
value += ledger.active;
ledger.active = Zero::zero();
}
Self::deposit_event(RawEvent::Unbonded(ledger.stash.clone(), value));
let era = T::EraFinder::current().unwrap_or(Zero::zero()) + T::BondingDuration::get();
ledger.unlocking.push(UnlockChunk { value, era });
Self::update_ledger(&controller, &ledger);
}
}
#[weight = 400_000]
fn withdraw_unbonded(origin) {
let controller = ensure_signed(origin)?;
let mut ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
let (stash, old_total) = (ledger.stash.clone(), ledger.total);
if let Some(current_era) = T::EraFinder::current() {
ledger = ledger.consolidate_unlocked(current_era)
}
if ledger.unlocking.is_empty() && ledger.active.is_zero() {
Self::kill_stash(&stash)?;
T::Currency::remove_lock(STAKING_ID, &stash);
} else {
Self::update_ledger(&controller, &ledger);
}
if ledger.total < old_total {
let value = old_total - ledger.total;
Self::deposit_event(RawEvent::Withdrawn(stash, value));
}
}
#[weight = 750_000]
fn nominate_contracts(origin, targets: Vec<(<T::Lookup as StaticLookup>::Source, BalanceOf<T>)>) {
let controller = ensure_signed(origin)?;
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
let stash = &ledger.stash;
ensure!(!targets.is_empty(), Error::<T>::EmptyNominateTargets);
let targets = targets.into_iter()
.take(MAX_NOMINATIONS)
.map(|t| match T::Lookup::lookup(t.0) {
Ok(a) => Ok((a, t.1)),
Err(err) => Err(err),
})
.collect::<result::Result<Vec<(T::AccountId, BalanceOf<T>)>, _>>()?;
if !targets.iter().all(|t| T::ContractFinder::is_exists_contract(&(t.0))) {
Err(Error::<T>::NotOperatedContracts)?
}
if targets
.iter()
.fold(BalanceOf::<T>::zero(),
|sum, t| sum.saturating_add(t.1)) > ledger.active {
Err(Error::<T>::NotEnoughStaking)?
}
let nominations = Nominations {
targets,
submitted_in: T::EraFinder::current().unwrap_or(Zero::zero()),
suppressed: false,
};
Self::take_in_nominations(stash, nominations);
Self::deposit_event(RawEvent::Nominate(stash.clone()));
}
#[weight = 500_000]
fn chill(origin) {
let controller = ensure_signed(origin)?;
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
Self::chill_stash(&ledger.stash);
}
#[weight = 500_000]
fn set_payee(origin, payee: RewardDestination) {
let controller = ensure_signed(origin)?;
let ledger = Self::ledger(&controller).ok_or(Error::<T>::NotController)?;
let stash = &ledger.stash;
<Payee<T>>::insert(stash, payee);
}
#[weight = 750_000]
fn set_controller(origin, controller: <T::Lookup as StaticLookup>::Source) {
let stash = ensure_signed(origin)?;
let old_controller = Self::bonded(&stash).ok_or(Error::<T>::NotStash)?;
let controller = T::Lookup::lookup(controller)?;
if <Ledger<T>>::contains_key(&controller) {
Err("controller already paired")?
}
if controller != old_controller {
<Bonded<T>>::insert(&stash, &controller);
if let Some(l) = <Ledger<T>>::take(&old_controller) {
<Ledger<T>>::insert(&controller, l);
}
}
}
#[weight = 1_000]
fn claim_for_nominator(origin, era: EraIndex) {
let nominator = ensure_signed(origin)?;
if let Some(active_era) = T::EraFinder::active() {
if era >= active_era.index {
Err("cannot claim yet")?
}
}
if let Some(current_era) = T::EraFinder::current() {
if era < current_era.saturating_sub(T::HistoryDepthFinder::get()) {
Err("the era is expired")?
}
}
let mut untreated_era = Self::nominators_untreated_era(&nominator);
if era == untreated_era {
Err("the nominator already rewarded")?
}
while era > untreated_era {
Self::propagate_nominate_totals(&nominator, &untreated_era, &(untreated_era + 1));
untreated_era += 1;
}
<NominatorsUntreatedEra<T>>::insert(&nominator, untreated_era);
for (contract, _) in <ErasStakingPoints<T>>::iter_prefix(&era) {
let mut untreated_era = Self::contracts_untreated_era(&contract);
if era != untreated_era {
while era > untreated_era {
Self::propagate_eras_staking_points_total(&contract, &untreated_era, &(untreated_era + 1));
untreated_era += 1;
}
<ContractsUntreatedEra<T>>::insert(&contract, untreated_era);
}
}
let rewards = match T::ForDappsEraReward::get(&era) {
Some(rewards) => rewards,
None => {
frame_support::print("Error: start_session_index must be set for current_era");
BalanceOf::<T>::zero()
}
};
let actual_rewarded = Self::reward_nominator(&era, rewards, &nominator);
Self::deposit_event(RawEvent::TotalDappsRewards(era, actual_rewarded));
}
#[weight = 1_000]
fn claim_for_operator(origin, era: EraIndex) {
let operator = ensure_signed(origin)?;
if let Some(active_era) = T::EraFinder::active() {
if era >= active_era.index {
Err("cannot claim yet")?
}
}
if let Some(current_era) = T::EraFinder::current() {
if era < current_era.saturating_sub(T::HistoryDepthFinder::get()) {
Err("the era is expired")?
}
}
let mut untreated_era = Self::operators_untreated_era(&operator);
if era == untreated_era {
Err("the operator already rewarded")?
}
while era > untreated_era {
Self::propagate_eras_staked_operators(&operator, &untreated_era, &(untreated_era + 1));
untreated_era += 1;
}
<OperatorsUntreatedEra<T>>::insert(&operator, untreated_era);
let rewards = match T::ForDappsEraReward::get(&era) {
Some(rewards) => rewards,
None => {
frame_support::print("Error: start_session_index must be set for current_era");
BalanceOf::<T>::zero()
}
};
let actual_rewarded = Self::reward_operator(&era, rewards, &operator);
Self::deposit_event(RawEvent::TotalDappsRewards(era, actual_rewarded));
}
}
}
fn migrate<T: Trait>() {}
impl<T: Trait> Module<T> {
fn update_ledger(
controller: &T::AccountId,
ledger: &StakingLedger<T::AccountId, BalanceOf<T>>,
) {
T::Currency::set_lock(
STAKING_ID,
&ledger.stash,
ledger.total,
WithdrawReasons::all(),
);
<Ledger<T>>::insert(controller, ledger);
}
fn kill_stash(stash: &T::AccountId) -> DispatchResult {
let controller = Bonded::<T>::take(stash).ok_or(Error::<T>::NotStash)?;
<Ledger<T>>::remove(&controller);
<Payee<T>>::remove(stash);
if let Some(nominations) = Self::dapps_nominations(stash) {
Self::remove_nominations(stash, nominations);
}
system::Module::<T>::dec_ref(stash);
Ok(())
}
fn chill_stash(stash: &T::AccountId) {
if let Some(nominations) = Self::dapps_nominations(stash) {
Self::remove_nominations(stash, nominations);
}
}
fn propagate_nominate_totals(nominator: &T::AccountId, src_era: &EraIndex, dst_era: &EraIndex) {
if <ErasNominateTotals<T>>::contains_key(src_era, nominator) {
let untreated_nootate_total = <ErasNominateTotals<T>>::get(src_era, nominator);
<ErasNominateTotals<T>>::mutate(dst_era, nominator, |total| {
*total += untreated_nootate_total;
})
}
}
fn reward_nominator(
era: &EraIndex,
max_payout: BalanceOf<T>,
nominator: &T::AccountId,
) -> BalanceOf<T> {
let mut total_imbalance = <PositiveImbalanceOf<T>>::zero();
let (_, nominators_reward) =
T::ComputeRewardsForDapps::compute_rewards_for_dapps(max_payout);
let total_staked = Self::eras_total_stake(era);
let nominate_values = (era.saturating_sub(T::HistoryDepthFinder::get())..=*era)
.flat_map(|e| <ErasStakingPoints<T>>::iter_prefix(&e))
.flat_map(|(_, points)| points.individual)
.filter(|(account, _)| *account == *nominator)
.map(|(_, value)| value)
.collect::<Vec<_>>();
let nominate_total = Self::eras_nominate_totals(era, nominator);
let reward = T::ComputeRewardsForDapps::compute_reward_for_nominator(
nominate_total,
total_staked,
nominators_reward,
nominate_values,
);
total_imbalance.subsume(
Self::make_payout(nominator, reward).unwrap_or(PositiveImbalanceOf::<T>::zero()),
);
let total_payout = total_imbalance.peek();
let rest = max_payout.saturating_sub(total_payout.clone());
T::Reward::on_unbalanced(total_imbalance);
T::RewardRemainder::on_unbalanced(T::Currency::issue(rest));
total_payout
}
fn propagate_eras_staked_operators(
operator: &T::AccountId,
src_era: &EraIndex,
dst_era: &EraIndex,
) {
if <ErasStakedOperators<T>>::contains_key(src_era, operator) {
let untreated_staked_operator = <ErasStakedOperators<T>>::get(src_era, operator);
<ErasStakedOperators<T>>::mutate(dst_era, operator, |total| {
*total += untreated_staked_operator;
});
}
}
fn reward_operator(
era: &EraIndex,
max_payout: BalanceOf<T>,
operator: &T::AccountId,
) -> BalanceOf<T> {
let mut total_imbalance = <PositiveImbalanceOf<T>>::zero();
let (operators_reward, _) =
T::ComputeRewardsForDapps::compute_rewards_for_dapps(max_payout);
let total_staked = Self::eras_total_stake(era);
let staked_operator = Self::eras_staked_operators(era, operator);
let reward = T::ComputeRewardsForDapps::compute_reward_for_operator(
staked_operator,
total_staked,
operators_reward,
);
total_imbalance.subsume(
T::Currency::deposit_into_existing(operator, reward)
.unwrap_or(PositiveImbalanceOf::<T>::zero()),
);
let total_payout = total_imbalance.peek();
let rest = max_payout.saturating_sub(total_payout.clone());
T::Reward::on_unbalanced(total_imbalance);
T::RewardRemainder::on_unbalanced(T::Currency::issue(rest));
total_payout
}
fn propagate_eras_staking_points_total(
contract: &T::AccountId,
src_era: &EraIndex,
dst_era: &EraIndex,
) {
if <ErasStakingPoints<T>>::contains_key(src_era, contract) {
let untreated_points = <ErasStakingPoints<T>>::get(src_era, contract);
<ErasStakingPoints<T>>::mutate(&dst_era, &contract, |points| {
(*points).total += untreated_points.total.clone();
});
}
}
fn compute_total_stake(era: &EraIndex) -> BalanceOf<T> {
let mut untreated_era = Self::untreated_era();
while *era > untreated_era {
let total = Self::eras_total_stake(&untreated_era);
<ErasTotalStake<T>>::mutate(&untreated_era + 1, |next_total| *next_total += total);
untreated_era += 1;
}
UntreatedEra::put(untreated_era);
let total_staked = Self::eras_total_stake(era);
total_staked
}
fn make_payout(stash: &T::AccountId, amount: BalanceOf<T>) -> Option<PositiveImbalanceOf<T>> {
let dest = Self::payee(stash);
match dest {
RewardDestination::Controller => Self::bonded(stash).and_then(|controller| {
T::Currency::deposit_into_existing(&controller, amount).ok()
}),
RewardDestination::Stash => T::Currency::deposit_into_existing(stash, amount).ok(),
RewardDestination::Staked => Self::bonded(stash)
.and_then(|c| Self::ledger(&c).map(|l| (c, l)))
.and_then(|(controller, mut l)| {
l.active += amount;
l.total += amount;
let r = T::Currency::deposit_into_existing(stash, amount).ok();
Self::update_ledger(&controller, &l);
r
}),
}
}
fn take_in_nominations(
stash: &T::AccountId,
nominations: Nominations<T::AccountId, BalanceOf<T>>,
) {
if let Some(current_era) = T::EraFinder::current() {
let next_era = current_era + 1;
for (contract, value) in nominations.targets.iter() {
if <ErasStakingPoints<T>>::contains_key(&next_era, &contract) {
<ErasStakingPoints<T>>::mutate(&next_era, &contract, |points| {
(*points).total += value.clone();
(*points).individual.insert(stash.clone(), value.clone());
});
} else {
let points = EraStakingPoints {
total: value.clone(),
individual: vec![(stash.clone(), value.clone())]
.into_iter()
.collect::<BTreeMap<T::AccountId, BalanceOf<T>>>(),
};
<ErasStakingPoints<T>>::insert(&next_era, &contract, points);
}
<ErasNominateTotals<T>>::mutate(&next_era, stash, |total| {
*total += value.clone();
});
if let Some(operator) = T::ContractFinder::operator(&contract) {
<ErasStakedOperators<T>>::mutate(&next_era, operator, |total| {
*total += value.clone();
});
}
<ErasTotalStake<T>>::mutate(&next_era, |total| {
*total += value.clone();
});
}
}
<DappsNominations<T>>::insert(stash, nominations);
}
fn remove_nominations(
stash: &T::AccountId,
nominations: Nominations<T::AccountId, BalanceOf<T>>,
) {
let era = nominations.submitted_in + 1;
for (contract, value) in nominations.targets.iter() {
<ErasStakingPoints<T>>::mutate(&era, &contract, |points| {
(*points).total = points.total.saturating_sub(value.clone());
(*points).individual.remove(stash);
});
<ErasNominateTotals<T>>::mutate(&era, stash, |total| {
*total = total.saturating_sub(value.clone());
});
if let Some(operator) = T::ContractFinder::operator(&contract) {
<ErasStakedOperators<T>>::mutate(&era, &operator, |total| {
*total = total.saturating_sub(value.clone());
});
}
<ErasTotalStake<T>>::mutate(&era, |total| {
*total = total.saturating_sub(value.clone());
});
}
<DappsNominations<T>>::remove(stash);
}
}
impl<T: Trait> ComputeEraWithParam<EraIndex> for Module<T> {
type Param = BalanceOf<T>;
fn compute(era: &EraIndex) -> BalanceOf<T> {
Self::compute_total_stake(era)
}
}