feat: Rebrand Polkadot/Substrate references to PezkuwiChain
This commit systematically rebrands various references from Parity Technologies' Polkadot/Substrate ecosystem to PezkuwiChain within the kurdistan-sdk. Key changes include: - Updated external repository URLs (zombienet-sdk, parity-db, parity-scale-codec, wasm-instrument) to point to pezkuwichain forks. - Modified internal documentation and code comments to reflect PezkuwiChain naming and structure. - Replaced direct references to with or specific paths within the for XCM, Pezkuwi, and other modules. - Cleaned up deprecated issue and PR references in various and files, particularly in and modules. - Adjusted image and logo URLs in documentation to point to PezkuwiChain assets. - Removed or rephrased comments related to external Polkadot/Substrate PRs and issues. This is a significant step towards fully customizing the SDK for the PezkuwiChain ecosystem.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Implementations of public traits, namely [`DelegationInterface`] and [`OnStakingUpdate`].
|
||||
|
||||
use super::*;
|
||||
use pezsp_staking::{DelegationInterface, DelegationMigrator, OnStakingUpdate};
|
||||
|
||||
impl<T: Config> DelegationInterface for Pallet<T> {
|
||||
type Balance = BalanceOf<T>;
|
||||
type AccountId = T::AccountId;
|
||||
|
||||
/// Effective balance of the `Agent` account.
|
||||
fn agent_balance(agent: Agent<Self::AccountId>) -> Option<Self::Balance> {
|
||||
AgentLedgerOuter::<T>::get(&agent.get())
|
||||
.map(|a| a.ledger.effective_balance())
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn agent_transferable_balance(agent: Agent<Self::AccountId>) -> Option<Self::Balance> {
|
||||
AgentLedgerOuter::<T>::get(&agent.get())
|
||||
.map(|a| a.ledger.unclaimed_withdrawals)
|
||||
.ok()
|
||||
}
|
||||
|
||||
fn delegator_balance(delegator: Delegator<Self::AccountId>) -> Option<Self::Balance> {
|
||||
Delegation::<T>::get(&delegator.get()).map(|d| d.amount)
|
||||
}
|
||||
|
||||
/// Delegate funds to an `Agent`.
|
||||
fn register_agent(
|
||||
agent: Agent<Self::AccountId>,
|
||||
reward_account: &Self::AccountId,
|
||||
) -> DispatchResult {
|
||||
Pallet::<T>::register_agent(
|
||||
RawOrigin::Signed(agent.clone().get()).into(),
|
||||
reward_account.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Remove `Agent` registration.
|
||||
fn remove_agent(agent: Agent<Self::AccountId>) -> DispatchResult {
|
||||
Pallet::<T>::remove_agent(RawOrigin::Signed(agent.clone().get()).into())
|
||||
}
|
||||
|
||||
/// Add more delegation to the `Agent` account.
|
||||
fn delegate(
|
||||
who: Delegator<Self::AccountId>,
|
||||
agent: Agent<Self::AccountId>,
|
||||
amount: Self::Balance,
|
||||
) -> DispatchResult {
|
||||
Pallet::<T>::delegate_to_agent(RawOrigin::Signed(who.get()).into(), agent.get(), amount)
|
||||
}
|
||||
|
||||
/// Withdraw delegation of `delegator` to `Agent`.
|
||||
///
|
||||
/// If there are funds in `Agent` account that can be withdrawn, then those funds would be
|
||||
/// unlocked/released in the delegator's account.
|
||||
fn withdraw_delegation(
|
||||
delegator: Delegator<Self::AccountId>,
|
||||
agent: Agent<Self::AccountId>,
|
||||
amount: Self::Balance,
|
||||
num_slashing_spans: u32,
|
||||
) -> DispatchResult {
|
||||
Pallet::<T>::release_delegation(
|
||||
RawOrigin::Signed(agent.get()).into(),
|
||||
delegator.get(),
|
||||
amount,
|
||||
num_slashing_spans,
|
||||
)
|
||||
}
|
||||
|
||||
/// Returns pending slash of the `agent`.
|
||||
fn pending_slash(agent: Agent<Self::AccountId>) -> Option<Self::Balance> {
|
||||
AgentLedgerOuter::<T>::get(&agent.get()).map(|d| d.ledger.pending_slash).ok()
|
||||
}
|
||||
|
||||
fn delegator_slash(
|
||||
agent: Agent<Self::AccountId>,
|
||||
delegator: Delegator<Self::AccountId>,
|
||||
value: Self::Balance,
|
||||
maybe_reporter: Option<Self::AccountId>,
|
||||
) -> pezsp_runtime::DispatchResult {
|
||||
Pallet::<T>::do_slash(agent, delegator, value, maybe_reporter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> DelegationMigrator for Pallet<T> {
|
||||
type Balance = BalanceOf<T>;
|
||||
type AccountId = T::AccountId;
|
||||
|
||||
fn migrate_nominator_to_agent(
|
||||
agent: Agent<Self::AccountId>,
|
||||
reward_account: &Self::AccountId,
|
||||
) -> DispatchResult {
|
||||
Pallet::<T>::migrate_to_agent(RawOrigin::Signed(agent.get()).into(), reward_account.clone())
|
||||
}
|
||||
fn migrate_delegation(
|
||||
agent: Agent<Self::AccountId>,
|
||||
delegator: Delegator<Self::AccountId>,
|
||||
value: Self::Balance,
|
||||
) -> DispatchResult {
|
||||
Pallet::<T>::migrate_delegation(
|
||||
RawOrigin::Signed(agent.get()).into(),
|
||||
delegator.get(),
|
||||
value,
|
||||
)
|
||||
}
|
||||
|
||||
/// Only used for testing.
|
||||
#[cfg(feature = "runtime-benchmarks")]
|
||||
fn force_kill_agent(agent: Agent<Self::AccountId>) {
|
||||
<Agents<T>>::remove(agent.clone().get());
|
||||
<Delegators<T>>::iter()
|
||||
.filter(|(_, delegation)| delegation.agent == agent.clone().get())
|
||||
.for_each(|(delegator, _)| {
|
||||
let _ = T::Currency::release_all(
|
||||
&HoldReason::StakingDelegation.into(),
|
||||
&delegator,
|
||||
Precision::BestEffort,
|
||||
);
|
||||
<Delegators<T>>::remove(&delegator);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> OnStakingUpdate<T::AccountId, BalanceOf<T>> for Pallet<T> {
|
||||
fn on_slash(
|
||||
who: &T::AccountId,
|
||||
_slashed_active: BalanceOf<T>,
|
||||
_slashed_unlocking: &alloc::collections::btree_map::BTreeMap<EraIndex, BalanceOf<T>>,
|
||||
slashed_total: BalanceOf<T>,
|
||||
) {
|
||||
<Agents<T>>::mutate(who, |maybe_register| match maybe_register {
|
||||
// if existing agent, register the slashed amount as pending slash.
|
||||
Some(register) => register.pending_slash.saturating_accrue(slashed_total),
|
||||
None => {
|
||||
// nothing to do
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
fn on_withdraw(stash: &T::AccountId, amount: BalanceOf<T>) {
|
||||
// if there is a withdraw to the agent, then add it to the unclaimed withdrawals.
|
||||
let _ = AgentLedgerOuter::<T>::get(stash)
|
||||
// can't do anything if there is an overflow error. Just raise a defensive error.
|
||||
.and_then(|agent| agent.add_unclaimed_withdraw(amount).defensive())
|
||||
.map(|agent| agent.save());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,832 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! # Delegated Staking Pallet
|
||||
//!
|
||||
//! This pallet implements [`pezsp_staking::DelegationInterface`] that provides delegation
|
||||
//! functionality to `delegators` and `agents`. It is designed to be used in conjunction with
|
||||
//! [`StakingInterface`] and relies on [`Config::CoreStaking`] to provide primitive staking
|
||||
//! functions.
|
||||
//!
|
||||
//! Currently, it does not expose any dispatchable calls but is written with a vision to expose them
|
||||
//! in the future such that it can be utilised by any external account, off-chain entity or xcm
|
||||
//! `MultiLocation` such as a teyrchain or a smart contract.
|
||||
//!
|
||||
//! ## Key Terminologies
|
||||
//! - **Agent**: An account who accepts delegations from other accounts and act as an agent on their
|
||||
//! behalf for staking these delegated funds. Also, sometimes referred as `Delegatee`.
|
||||
//! - **Delegator**: An account who delegates their funds to an `agent` and authorises them to use
|
||||
//! it for staking.
|
||||
//! - **AgentLedger**: A data structure that holds important information about the `agent` such as
|
||||
//! total delegations they have received, any slashes posted to them, etc.
|
||||
//! - **Delegation**: A data structure that stores the amount of funds delegated to an `agent` by a
|
||||
//! `delegator`.
|
||||
//!
|
||||
//! ## Goals
|
||||
//!
|
||||
//! Direct nomination on the Staking pallet does not scale well. Nominations pools were created to
|
||||
//! address this by pooling delegator funds into one account and then staking it. This though had
|
||||
//! a very critical limitation that the funds were moved from delegator account to pool account
|
||||
//! and hence the delegator lost control over their funds for using it for other purposes such as
|
||||
//! governance. This pallet aims to solve this by extending the staking pallet to support a new
|
||||
//! primitive function: delegation of funds to an `agent` with the intent of staking. The agent can
|
||||
//! then stake the delegated funds to [`Config::CoreStaking`] on behalf of the delegators.
|
||||
//!
|
||||
//! ### Withdrawal Management
|
||||
//! Agent unbonding does not regulate ordering of consequent withdrawal for delegators. This is upto
|
||||
//! the consumer of this pallet to implement in what order unbondable funds from
|
||||
//! [`Config::CoreStaking`] can be withdrawn by the delegators.
|
||||
//!
|
||||
//! ### Reward and Slashing
|
||||
//! This pallet does not enforce any specific strategy for how rewards or slashes are applied. It
|
||||
//! is upto the `agent` account to decide how to apply the rewards and slashes.
|
||||
//!
|
||||
//! This importantly allows clients of this pallet to build their own strategies for reward/slashes.
|
||||
//! For example, an `agent` account can choose to first slash the reward pot before slashing the
|
||||
//! delegators. Or part of the reward can go to an insurance fund that can be used to cover any
|
||||
//! potential future slashes. The goal is to eventually allow foreign MultiLocations
|
||||
//! (smart contracts or pallets on another chain) to build their own pooled staking solutions
|
||||
//! similar to `NominationPools`.
|
||||
|
||||
//! ## Core functions
|
||||
//!
|
||||
//! - Allow an account to receive delegations. See [`Pallet::register_agent`].
|
||||
//! - Delegate funds to an `agent` account. See [`Pallet::delegate_to_agent`].
|
||||
//! - Release delegated funds from an `agent` account to the `delegator`. See
|
||||
//! [`Pallet::release_delegation`].
|
||||
//! - Migrate a `Nominator` account to an `agent` account. See [`Pallet::migrate_to_agent`].
|
||||
//! Explained in more detail in the `Migration` section.
|
||||
//! - Migrate unclaimed delegated funds from `agent` to delegator. When a nominator migrates to an
|
||||
//! agent, the funds are held in a proxy account. This function allows the delegator to claim
|
||||
//! their share of the funds from the proxy account. See [`Pallet::migrate_delegation`].
|
||||
//!
|
||||
//! ## Lazy Slashing
|
||||
//! One of the reasons why direct nominators on staking pallet cannot scale well is because all
|
||||
//! nominators are slashed at the same time. This is expensive and needs to be bounded operation.
|
||||
//!
|
||||
//! This pallet implements a lazy slashing mechanism. Any slashes to the `agent` are posted in its
|
||||
//! `AgentLedger` as a pending slash. Since the actual amount is held in the multiple
|
||||
//! `delegator` accounts, this pallet has no way to know how to apply slash. It is the `agent`'s
|
||||
//! responsibility to apply slashes for each delegator, one at a time. Staking pallet ensures the
|
||||
//! pending slash never exceeds staked amount and would freeze further withdraws until all pending
|
||||
//! slashes are cleared.
|
||||
//!
|
||||
//! The user of this pallet can apply slash using
|
||||
//! [DelegationInterface::delegator_slash](pezsp_staking::DelegationInterface::delegator_slash).
|
||||
//!
|
||||
//! ## Migration from Nominator to Agent
|
||||
//! More details [here](https://hackmd.io/@ak0n/454-np-governance).
|
||||
//!
|
||||
//! ## Nomination Pool vs Delegation Staking
|
||||
//! This pallet is not a replacement for Nomination Pool but adds a new primitive in addition to
|
||||
//! staking pallet that can be used by Nomination Pool to support delegation based staking. It can
|
||||
//! be thought of as an extension to the Staking Pallet in relation to Nomination Pools.
|
||||
//! Technically, these changes could be made in one of those pallets as well but that would have
|
||||
//! meant significant refactoring and high chances of introducing a regression. With this approach,
|
||||
//! we can keep the existing pallets with minimal changes and introduce a new pallet that can be
|
||||
//! optionally used by Nomination Pool. The vision is to build this in a configurable way such that
|
||||
//! runtime can choose whether to use this pallet or not.
|
||||
//!
|
||||
//! With that said, following is the main difference between
|
||||
//! #### Nomination Pool without delegation support
|
||||
//! 1) transfer fund from delegator to pool account, and
|
||||
//! 2) stake from pool account as a direct nominator.
|
||||
//!
|
||||
//! #### Nomination Pool with delegation support
|
||||
//! 1) delegate fund from delegator to pool account, and
|
||||
//! 2) stake from pool account as an `Agent` account on the staking pallet.
|
||||
//!
|
||||
//! The difference being, in the second approach, the delegated funds will be locked in-place in
|
||||
//! user's account enabling them to participate in use cases that allows use of `held` funds such
|
||||
//! as participation in governance voting.
|
||||
//!
|
||||
//! Nomination pool still does all the heavy lifting around pool administration, reward
|
||||
//! distribution, lazy slashing and as such, is not meant to be replaced with this pallet.
|
||||
//!
|
||||
//! ## Limitations
|
||||
//! - Rewards can not be auto-compounded.
|
||||
//! - Slashes are lazy and hence there could be a period of time when an account can use funds for
|
||||
//! operations such as voting in governance even though they should be slashed.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
#![deny(rustdoc::broken_intra_doc_links)]
|
||||
|
||||
mod impls;
|
||||
pub mod migration;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod types;
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
pub use pallet::*;
|
||||
|
||||
use types::*;
|
||||
|
||||
use core::convert::TryInto;
|
||||
use pezframe_support::{
|
||||
pezpallet_prelude::*,
|
||||
traits::{
|
||||
fungible::{
|
||||
hold::{
|
||||
Balanced as FunHoldBalanced, Inspect as FunHoldInspect, Mutate as FunHoldMutate,
|
||||
},
|
||||
Balanced, Inspect as FunInspect, Mutate as FunMutate,
|
||||
},
|
||||
tokens::{fungible::Credit, Fortitude, Precision, Preservation, Restriction},
|
||||
Defensive, DefensiveOption, Imbalance, OnUnbalanced,
|
||||
},
|
||||
};
|
||||
use pezsp_io::hashing::blake2_256;
|
||||
use pezsp_runtime::{
|
||||
traits::{CheckedAdd, CheckedSub, TrailingZeroInput, Zero},
|
||||
ArithmeticError, DispatchResult, Perbill, RuntimeDebug, Saturating,
|
||||
};
|
||||
use pezsp_staking::{Agent, Delegator, EraIndex, StakingInterface, StakingUnchecked};
|
||||
|
||||
/// The log target of this pallet.
|
||||
pub const LOG_TARGET: &str = "runtime::delegated-staking";
|
||||
// syntactic sugar for logging.
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($level:tt, $patter:expr $(, $values:expr)* $(,)?) => {
|
||||
log::$level!(
|
||||
target: $crate::LOG_TARGET,
|
||||
concat!("[{:?}] 🏊♂️ ", $patter), <pezframe_system::Pallet<T>>::block_number() $(, $values)*
|
||||
)
|
||||
};
|
||||
}
|
||||
pub type BalanceOf<T> =
|
||||
<<T as Config>::Currency as FunInspect<<T as pezframe_system::Config>::AccountId>>::Balance;
|
||||
|
||||
use pezframe_system::{ensure_signed, pezpallet_prelude::*, RawOrigin};
|
||||
|
||||
#[pezframe_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
|
||||
/// The in-code storage version.
|
||||
const STORAGE_VERSION: StorageVersion = StorageVersion::new(0);
|
||||
#[pallet::pallet]
|
||||
#[pallet::storage_version(STORAGE_VERSION)]
|
||||
pub struct Pallet<T>(PhantomData<T>);
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: pezframe_system::Config {
|
||||
/// The overarching event type.
|
||||
#[allow(deprecated)]
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// Injected identifier for the pallet.
|
||||
#[pallet::constant]
|
||||
type PalletId: Get<pezframe_support::PalletId>;
|
||||
|
||||
/// Currency type.
|
||||
type Currency: FunHoldMutate<Self::AccountId, Reason = Self::RuntimeHoldReason>
|
||||
+ FunMutate<Self::AccountId>
|
||||
+ FunHoldBalanced<Self::AccountId>;
|
||||
|
||||
/// Handler for the unbalanced reduction when slashing a delegator.
|
||||
type OnSlash: OnUnbalanced<Credit<Self::AccountId, Self::Currency>>;
|
||||
|
||||
/// Fraction of the slash that is rewarded to the caller of pending slash to the agent.
|
||||
#[pallet::constant]
|
||||
type SlashRewardFraction: Get<Perbill>;
|
||||
|
||||
/// Overarching hold reason.
|
||||
type RuntimeHoldReason: From<HoldReason>;
|
||||
|
||||
/// Core staking implementation.
|
||||
type CoreStaking: StakingUnchecked<Balance = BalanceOf<Self>, AccountId = Self::AccountId>;
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The account cannot perform this operation.
|
||||
NotAllowed,
|
||||
/// An existing staker cannot perform this action.
|
||||
AlreadyStaking,
|
||||
/// Reward Destination cannot be same as `Agent` account.
|
||||
InvalidRewardDestination,
|
||||
/// Delegation conditions are not met.
|
||||
///
|
||||
/// Possible issues are
|
||||
/// 1) Cannot delegate to self,
|
||||
/// 2) Cannot delegate to multiple delegates.
|
||||
InvalidDelegation,
|
||||
/// The account does not have enough funds to perform the operation.
|
||||
NotEnoughFunds,
|
||||
/// Not an existing `Agent` account.
|
||||
NotAgent,
|
||||
/// Not a Delegator account.
|
||||
NotDelegator,
|
||||
/// Some corruption in internal state.
|
||||
BadState,
|
||||
/// Unapplied pending slash restricts operation on `Agent`.
|
||||
UnappliedSlash,
|
||||
/// `Agent` has no pending slash to be applied.
|
||||
NothingToSlash,
|
||||
/// Failed to withdraw amount from Core Staking.
|
||||
WithdrawFailed,
|
||||
/// Operation not supported by this pallet.
|
||||
NotSupported,
|
||||
}
|
||||
|
||||
/// A reason for placing a hold on funds.
|
||||
#[pallet::composite_enum]
|
||||
pub enum HoldReason {
|
||||
/// Funds held for stake delegation to another account.
|
||||
#[codec(index = 0)]
|
||||
StakingDelegation,
|
||||
}
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub (super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// Funds delegated by a delegator.
|
||||
Delegated { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
|
||||
/// Funds released to a delegator.
|
||||
Released { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
|
||||
/// Funds slashed from a delegator.
|
||||
Slashed { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
|
||||
/// Unclaimed delegation funds migrated to delegator.
|
||||
MigratedDelegation { agent: T::AccountId, delegator: T::AccountId, amount: BalanceOf<T> },
|
||||
}
|
||||
|
||||
/// Map of Delegators to their `Delegation`.
|
||||
///
|
||||
/// Implementation note: We are not using a double map with `delegator` and `agent` account
|
||||
/// as keys since we want to restrict delegators to delegate only to one account at a time.
|
||||
#[pallet::storage]
|
||||
pub type Delegators<T: Config> =
|
||||
CountedStorageMap<_, Twox64Concat, T::AccountId, Delegation<T>, OptionQuery>;
|
||||
|
||||
/// Map of `Agent` to their `Ledger`.
|
||||
#[pallet::storage]
|
||||
pub type Agents<T: Config> =
|
||||
CountedStorageMap<_, Twox64Concat, T::AccountId, AgentLedger<T>, OptionQuery>;
|
||||
|
||||
// This pallet is not currently written with the intention of exposing any calls. But the
|
||||
// functions defined in the following impl block should act as a good reference for how the
|
||||
// exposed calls would look like when exposed.
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Register an account to become a stake `Agent`. Sometimes also called a `Delegatee`.
|
||||
///
|
||||
/// Delegators can authorize `Agent`s to stake on their behalf by delegating their funds to
|
||||
/// them. The `Agent` can then use the delegated funds to stake to [`Config::CoreStaking`].
|
||||
///
|
||||
/// An account that is directly staked to [`Config::CoreStaking`] cannot become an `Agent`.
|
||||
/// However, they can migrate to become an agent using [`Self::migrate_to_agent`].
|
||||
///
|
||||
/// Implementation note: This function allows any account to become an agent. It is
|
||||
/// important though that accounts that call [`StakingUnchecked::virtual_bond`] are keyless
|
||||
/// accounts. This is not a problem for now since this is only used by other pallets in the
|
||||
/// runtime which use keyless account as agents. If we later want to expose this as a
|
||||
/// dispatchable call, we should derive a sub-account from the caller and use that as the
|
||||
/// agent account.
|
||||
pub fn register_agent(
|
||||
origin: OriginFor<T>,
|
||||
reward_account: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Existing `agent` cannot register again and a delegator cannot become an `agent`.
|
||||
ensure!(!Self::is_agent(&who) && !Self::is_delegator(&who), Error::<T>::NotAllowed);
|
||||
|
||||
// Reward account cannot be same as `agent` account.
|
||||
ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);
|
||||
|
||||
Self::do_register_agent(&who, &reward_account);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove an account from being an `Agent`.
|
||||
///
|
||||
/// This can only be called if the agent has no delegated funds, no pending slashes and no
|
||||
/// unclaimed withdrawals.
|
||||
pub fn remove_agent(origin: OriginFor<T>) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
let ledger = AgentLedger::<T>::get(&who).ok_or(Error::<T>::NotAgent)?;
|
||||
|
||||
ensure!(
|
||||
ledger.total_delegated == Zero::zero() &&
|
||||
ledger.pending_slash == Zero::zero() &&
|
||||
ledger.unclaimed_withdrawals == Zero::zero(),
|
||||
Error::<T>::NotAllowed
|
||||
);
|
||||
|
||||
AgentLedger::<T>::remove(&who);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migrate from a `Nominator` account to `Agent` account.
|
||||
///
|
||||
/// The origin needs to
|
||||
/// - be a `Nominator` with [`Config::CoreStaking`],
|
||||
/// - not already an `Agent`,
|
||||
///
|
||||
/// This function will create a proxy account to the agent called `proxy_delegator` and
|
||||
/// transfer the directly staked amount by the agent to it. The `proxy_delegator` delegates
|
||||
/// the funds to the origin making origin an `Agent` account. The real `delegator`
|
||||
/// accounts of the origin can later migrate their funds using [Self::migrate_delegation] to
|
||||
/// claim back their share of delegated funds from `proxy_delegator` to self.
|
||||
///
|
||||
/// Any free fund in the agent's account will be marked as unclaimed withdrawal.
|
||||
pub fn migrate_to_agent(
|
||||
origin: OriginFor<T>,
|
||||
reward_account: T::AccountId,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
// ensure who is a staker in `CoreStaking` but not already an agent or a delegator.
|
||||
ensure!(
|
||||
Self::is_direct_staker(&who) && !Self::is_agent(&who) && !Self::is_delegator(&who),
|
||||
Error::<T>::NotAllowed
|
||||
);
|
||||
|
||||
// Reward account cannot be same as `agent` account.
|
||||
ensure!(reward_account != who, Error::<T>::InvalidRewardDestination);
|
||||
|
||||
Self::do_migrate_to_agent(&who, &reward_account)
|
||||
}
|
||||
|
||||
/// Release previously delegated funds by delegator to origin.
|
||||
///
|
||||
/// Only agents can call this.
|
||||
///
|
||||
/// Tries to withdraw unbonded funds from `CoreStaking` if needed and release amount to
|
||||
/// `delegator`.
|
||||
pub fn release_delegation(
|
||||
origin: OriginFor<T>,
|
||||
delegator: T::AccountId,
|
||||
amount: BalanceOf<T>,
|
||||
num_slashing_spans: u32,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
Self::do_release(
|
||||
Agent::from(who),
|
||||
Delegator::from(delegator),
|
||||
amount,
|
||||
num_slashing_spans,
|
||||
)
|
||||
}
|
||||
|
||||
/// Migrate delegated funds that are held in `proxy_delegator` to the claiming `delegator`'s
|
||||
/// account. If successful, the specified funds will be moved and delegated from `delegator`
|
||||
/// account to the agent.
|
||||
///
|
||||
/// This can be called by `agent` accounts that were previously a direct `Nominator` with
|
||||
/// [`Config::CoreStaking`] and has some remaining unclaimed delegations.
|
||||
///
|
||||
/// Internally, it moves some delegations from `proxy_delegator` account to `delegator`
|
||||
/// account and reapplying the holds.
|
||||
pub fn migrate_delegation(
|
||||
origin: OriginFor<T>,
|
||||
delegator: T::AccountId,
|
||||
amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
let agent = ensure_signed(origin)?;
|
||||
|
||||
// Ensure delegator is sane.
|
||||
ensure!(!Self::is_agent(&delegator), Error::<T>::NotAllowed);
|
||||
ensure!(!Self::is_delegator(&delegator), Error::<T>::NotAllowed);
|
||||
|
||||
// ensure agent is sane.
|
||||
ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
|
||||
|
||||
// and has enough delegated balance to migrate.
|
||||
let proxy_delegator = Self::generate_proxy_delegator(Agent::from(agent));
|
||||
let balance_remaining = Self::held_balance_of(proxy_delegator.clone());
|
||||
ensure!(balance_remaining >= amount, Error::<T>::NotEnoughFunds);
|
||||
|
||||
Self::do_migrate_delegation(proxy_delegator, Delegator::from(delegator), amount)
|
||||
}
|
||||
|
||||
/// Delegate given `amount` of tokens to an `Agent` account.
|
||||
///
|
||||
/// If `origin` is the first time delegator, we add them to state. If they are already
|
||||
/// delegating, we increase the delegation.
|
||||
///
|
||||
/// Conditions:
|
||||
/// - Delegators cannot delegate to more than one agent.
|
||||
/// - The `agent` account should already be registered as such. See
|
||||
/// [`Self::register_agent`].
|
||||
pub fn delegate_to_agent(
|
||||
origin: OriginFor<T>,
|
||||
agent: T::AccountId,
|
||||
amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
let delegator = ensure_signed(origin)?;
|
||||
|
||||
// ensure delegator is sane.
|
||||
ensure!(
|
||||
Delegation::<T>::can_delegate(&delegator, &agent),
|
||||
Error::<T>::InvalidDelegation
|
||||
);
|
||||
|
||||
// ensure agent is sane.
|
||||
ensure!(Self::is_agent(&agent), Error::<T>::NotAgent);
|
||||
|
||||
// add to delegation.
|
||||
Self::do_delegate(Delegator::from(delegator), Agent::from(agent.clone()), amount)?;
|
||||
|
||||
// bond the newly delegated amount to `CoreStaking`.
|
||||
Self::do_bond(Agent::from(agent), amount)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn try_state(_n: BlockNumberFor<T>) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
Self::do_try_state()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Derive an account from the migrating agent account where the unclaimed delegation funds
|
||||
/// are held.
|
||||
pub fn generate_proxy_delegator(agent: Agent<T::AccountId>) -> Delegator<T::AccountId> {
|
||||
Delegator::from(Self::sub_account(AccountType::ProxyDelegator, agent.get()))
|
||||
}
|
||||
|
||||
/// Derive a (keyless) pot account from the given agent account and account type.
|
||||
fn sub_account(account_type: AccountType, acc: T::AccountId) -> T::AccountId {
|
||||
let entropy = (T::PalletId::get(), acc, account_type).using_encoded(blake2_256);
|
||||
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed")
|
||||
}
|
||||
|
||||
/// Held balance of a delegator.
|
||||
pub(crate) fn held_balance_of(who: Delegator<T::AccountId>) -> BalanceOf<T> {
|
||||
T::Currency::balance_on_hold(&HoldReason::StakingDelegation.into(), &who.get())
|
||||
}
|
||||
|
||||
/// Returns true if who is registered as an `Agent`.
|
||||
fn is_agent(who: &T::AccountId) -> bool {
|
||||
<Agents<T>>::contains_key(who)
|
||||
}
|
||||
|
||||
/// Returns true if who is delegating to an `Agent` account.
|
||||
fn is_delegator(who: &T::AccountId) -> bool {
|
||||
<Delegators<T>>::contains_key(who)
|
||||
}
|
||||
|
||||
/// Returns true if who is already staking on [`Config::CoreStaking`].
|
||||
fn is_direct_staker(who: &T::AccountId) -> bool {
|
||||
T::CoreStaking::status(who).is_ok()
|
||||
}
|
||||
|
||||
/// Registers a new agent in the system.
|
||||
fn do_register_agent(who: &T::AccountId, reward_account: &T::AccountId) {
|
||||
// TODO: Consider taking a deposit for being an agent.
|
||||
AgentLedger::<T>::new(reward_account).update(who);
|
||||
}
|
||||
|
||||
/// Migrate existing staker account `who` to an `Agent` account.
|
||||
fn do_migrate_to_agent(who: &T::AccountId, reward_account: &T::AccountId) -> DispatchResult {
|
||||
Self::do_register_agent(who, reward_account);
|
||||
|
||||
// We create a proxy delegator that will keep all the delegation funds until funds are
|
||||
// transferred to actual delegator.
|
||||
let proxy_delegator = Self::generate_proxy_delegator(Agent::from(who.clone()));
|
||||
|
||||
// Get current stake
|
||||
let stake = T::CoreStaking::stake(who)?;
|
||||
|
||||
// release funds from core staking.
|
||||
T::CoreStaking::migrate_to_virtual_staker(who)?;
|
||||
|
||||
// transfer just released staked amount plus any free amount.
|
||||
let amount_to_transfer =
|
||||
T::Currency::reducible_balance(who, Preservation::Expendable, Fortitude::Polite);
|
||||
|
||||
// This should never fail but if it does, it indicates bad state and we abort.
|
||||
T::Currency::transfer(
|
||||
who,
|
||||
&proxy_delegator.clone().get(),
|
||||
amount_to_transfer,
|
||||
Preservation::Expendable,
|
||||
)?;
|
||||
|
||||
T::CoreStaking::set_payee(who, reward_account)?;
|
||||
// delegate all transferred funds back to agent.
|
||||
Self::do_delegate(proxy_delegator, Agent::from(who.clone()), amount_to_transfer)?;
|
||||
// if the transferred/delegated amount was greater than the stake, mark the extra as
|
||||
// unclaimed withdrawal.
|
||||
let unclaimed_withdraws = amount_to_transfer
|
||||
.checked_sub(&stake.total)
|
||||
.defensive_ok_or(ArithmeticError::Underflow)?;
|
||||
|
||||
if !unclaimed_withdraws.is_zero() {
|
||||
let mut ledger = AgentLedger::<T>::get(who).ok_or(Error::<T>::NotAgent)?;
|
||||
ledger.unclaimed_withdrawals = ledger
|
||||
.unclaimed_withdrawals
|
||||
.checked_add(&unclaimed_withdraws)
|
||||
.defensive_ok_or(ArithmeticError::Overflow)?;
|
||||
ledger.update(who);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Bond `amount` to `agent_acc` in [`Config::CoreStaking`].
|
||||
fn do_bond(agent_acc: Agent<T::AccountId>, amount: BalanceOf<T>) -> DispatchResult {
|
||||
let agent_ledger = AgentLedgerOuter::<T>::get(&agent_acc.get())?;
|
||||
|
||||
let available_to_bond = agent_ledger.available_to_bond();
|
||||
defensive_assert!(amount == available_to_bond, "not expected value to bond");
|
||||
|
||||
if agent_ledger.is_bonded() {
|
||||
T::CoreStaking::bond_extra(&agent_ledger.key, amount)
|
||||
} else {
|
||||
T::CoreStaking::virtual_bond(&agent_ledger.key, amount, agent_ledger.reward_account())
|
||||
}
|
||||
}
|
||||
|
||||
/// Delegate `amount` from `delegator` to `agent`.
|
||||
fn do_delegate(
|
||||
delegator: Delegator<T::AccountId>,
|
||||
agent: Agent<T::AccountId>,
|
||||
amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
// get inner type
|
||||
let agent = agent.get();
|
||||
let delegator = delegator.get();
|
||||
|
||||
let mut ledger = AgentLedger::<T>::get(&agent).ok_or(Error::<T>::NotAgent)?;
|
||||
|
||||
if let Some(mut existing_delegation) = Delegation::<T>::get(&delegator) {
|
||||
ensure!(existing_delegation.agent == agent, Error::<T>::InvalidDelegation);
|
||||
// update amount and return the updated delegation.
|
||||
existing_delegation.amount = existing_delegation
|
||||
.amount
|
||||
.checked_add(&amount)
|
||||
.ok_or(ArithmeticError::Overflow)?;
|
||||
existing_delegation
|
||||
} else {
|
||||
Delegation::<T>::new(&agent, amount)
|
||||
}
|
||||
.update(&delegator);
|
||||
|
||||
// try to hold the funds.
|
||||
T::Currency::hold(&HoldReason::StakingDelegation.into(), &delegator, amount)?;
|
||||
|
||||
ledger.total_delegated =
|
||||
ledger.total_delegated.checked_add(&amount).ok_or(ArithmeticError::Overflow)?;
|
||||
ledger.update(&agent);
|
||||
|
||||
Self::deposit_event(Event::<T>::Delegated { agent, delegator, amount });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Release `amount` of delegated funds from `agent` to `delegator`.
|
||||
fn do_release(
|
||||
who: Agent<T::AccountId>,
|
||||
delegator: Delegator<T::AccountId>,
|
||||
amount: BalanceOf<T>,
|
||||
num_slashing_spans: u32,
|
||||
) -> DispatchResult {
|
||||
// get inner type
|
||||
let agent = who.get();
|
||||
let delegator = delegator.get();
|
||||
|
||||
let mut agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
|
||||
let mut delegation = Delegation::<T>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
|
||||
|
||||
// make sure delegation to be released is sound.
|
||||
ensure!(delegation.agent == agent, Error::<T>::NotAgent);
|
||||
ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
|
||||
|
||||
// if we do not already have enough funds to be claimed, try to withdraw some more.
|
||||
if agent_ledger.ledger.unclaimed_withdrawals < amount {
|
||||
// withdraw account.
|
||||
T::CoreStaking::withdraw_unbonded(agent.clone(), num_slashing_spans)
|
||||
.map_err(|_| Error::<T>::WithdrawFailed)?;
|
||||
// reload agent from storage since withdrawal might have changed the state.
|
||||
agent_ledger = agent_ledger.reload()?;
|
||||
}
|
||||
|
||||
// if we still do not have enough funds to release, abort.
|
||||
ensure!(agent_ledger.ledger.unclaimed_withdrawals >= amount, Error::<T>::NotEnoughFunds);
|
||||
agent_ledger.remove_unclaimed_withdraw(amount)?.update();
|
||||
|
||||
delegation.amount = delegation
|
||||
.amount
|
||||
.checked_sub(&amount)
|
||||
.defensive_ok_or(ArithmeticError::Overflow)?;
|
||||
|
||||
let released = T::Currency::release(
|
||||
&HoldReason::StakingDelegation.into(),
|
||||
&delegator,
|
||||
amount,
|
||||
Precision::BestEffort,
|
||||
)?;
|
||||
|
||||
defensive_assert!(released == amount, "hold should have been released fully");
|
||||
|
||||
// update delegation.
|
||||
delegation.update(&delegator);
|
||||
|
||||
Self::deposit_event(Event::<T>::Released { agent, delegator, amount });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Migrates delegation of `amount` from `source` account to `destination` account.
|
||||
fn do_migrate_delegation(
|
||||
source_delegator: Delegator<T::AccountId>,
|
||||
destination_delegator: Delegator<T::AccountId>,
|
||||
amount: BalanceOf<T>,
|
||||
) -> DispatchResult {
|
||||
// get inner type
|
||||
let source_delegator = source_delegator.get();
|
||||
let destination_delegator = destination_delegator.get();
|
||||
|
||||
let mut source_delegation =
|
||||
Delegators::<T>::get(&source_delegator).defensive_ok_or(Error::<T>::BadState)?;
|
||||
|
||||
// ensure source has enough funds to migrate.
|
||||
ensure!(source_delegation.amount >= amount, Error::<T>::NotEnoughFunds);
|
||||
debug_assert!(
|
||||
!Self::is_delegator(&destination_delegator) && !Self::is_agent(&destination_delegator)
|
||||
);
|
||||
|
||||
let agent = source_delegation.agent.clone();
|
||||
// create a new delegation for destination delegator.
|
||||
Delegation::<T>::new(&agent, amount).update(&destination_delegator);
|
||||
|
||||
source_delegation.amount = source_delegation
|
||||
.amount
|
||||
.checked_sub(&amount)
|
||||
.defensive_ok_or(Error::<T>::BadState)?;
|
||||
|
||||
// transfer the held amount in `source_delegator` to `destination_delegator`.
|
||||
T::Currency::transfer_on_hold(
|
||||
&HoldReason::StakingDelegation.into(),
|
||||
&source_delegator,
|
||||
&destination_delegator,
|
||||
amount,
|
||||
Precision::Exact,
|
||||
Restriction::OnHold,
|
||||
Fortitude::Polite,
|
||||
)?;
|
||||
|
||||
// update source delegation.
|
||||
source_delegation.update(&source_delegator);
|
||||
|
||||
Self::deposit_event(Event::<T>::MigratedDelegation {
|
||||
agent,
|
||||
delegator: destination_delegator,
|
||||
amount,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Take slash `amount` from agent's `pending_slash`counter and apply it to `delegator` account.
|
||||
pub fn do_slash(
|
||||
agent: Agent<T::AccountId>,
|
||||
delegator: Delegator<T::AccountId>,
|
||||
amount: BalanceOf<T>,
|
||||
maybe_reporter: Option<T::AccountId>,
|
||||
) -> DispatchResult {
|
||||
// get inner type
|
||||
let agent = agent.get();
|
||||
let delegator = delegator.get();
|
||||
|
||||
let agent_ledger = AgentLedgerOuter::<T>::get(&agent)?;
|
||||
// ensure there is something to slash
|
||||
ensure!(agent_ledger.ledger.pending_slash > Zero::zero(), Error::<T>::NothingToSlash);
|
||||
|
||||
let mut delegation = <Delegators<T>>::get(&delegator).ok_or(Error::<T>::NotDelegator)?;
|
||||
ensure!(delegation.agent == agent.clone(), Error::<T>::NotAgent);
|
||||
ensure!(delegation.amount >= amount, Error::<T>::NotEnoughFunds);
|
||||
|
||||
// slash delegator
|
||||
let (mut credit, missing) =
|
||||
T::Currency::slash(&HoldReason::StakingDelegation.into(), &delegator, amount);
|
||||
|
||||
defensive_assert!(missing.is_zero(), "slash should have been fully applied");
|
||||
|
||||
let actual_slash = credit.peek();
|
||||
|
||||
// remove the applied slashed amount from agent.
|
||||
agent_ledger.remove_slash(actual_slash).save();
|
||||
delegation.amount =
|
||||
delegation.amount.checked_sub(&actual_slash).ok_or(ArithmeticError::Overflow)?;
|
||||
delegation.update(&delegator);
|
||||
|
||||
if let Some(reporter) = maybe_reporter {
|
||||
let reward_payout: BalanceOf<T> = T::SlashRewardFraction::get() * actual_slash;
|
||||
let (reporter_reward, rest) = credit.split(reward_payout);
|
||||
|
||||
// credit is the amount that we provide to `T::OnSlash`.
|
||||
credit = rest;
|
||||
|
||||
// reward reporter or drop it.
|
||||
let _ = T::Currency::resolve(&reporter, reporter_reward);
|
||||
}
|
||||
|
||||
T::OnSlash::on_unbalanced(credit);
|
||||
|
||||
Self::deposit_event(Event::<T>::Slashed { agent, delegator, amount });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Total balance that is available for stake. Includes already staked amount.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn stakeable_balance(who: Agent<T::AccountId>) -> BalanceOf<T> {
|
||||
AgentLedgerOuter::<T>::get(&who.get())
|
||||
.map(|agent| agent.ledger.stakeable_balance())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "try-runtime"))]
|
||||
use alloc::collections::btree_map::BTreeMap;
|
||||
|
||||
#[cfg(any(test, feature = "try-runtime"))]
|
||||
impl<T: Config> Pallet<T> {
|
||||
pub(crate) fn do_try_state() -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
// build map to avoid reading storage multiple times.
|
||||
let delegation_map = Delegators::<T>::iter().collect::<BTreeMap<_, _>>();
|
||||
let ledger_map = Agents::<T>::iter().collect::<BTreeMap<_, _>>();
|
||||
|
||||
Self::check_delegates(ledger_map.clone())?;
|
||||
Self::check_delegators(delegation_map, ledger_map)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_delegates(
|
||||
ledgers: BTreeMap<T::AccountId, AgentLedger<T>>,
|
||||
) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
for (agent, ledger) in ledgers {
|
||||
let staked_value = ledger.stakeable_balance();
|
||||
|
||||
if !staked_value.is_zero() {
|
||||
ensure!(
|
||||
matches!(
|
||||
T::CoreStaking::status(&agent).expect("agent should be bonded"),
|
||||
pezsp_staking::StakerStatus::Nominator(_) | pezsp_staking::StakerStatus::Idle
|
||||
),
|
||||
"agent should be bonded and not validator"
|
||||
);
|
||||
}
|
||||
|
||||
ensure!(
|
||||
ledger.stakeable_balance() >=
|
||||
T::CoreStaking::total_stake(&agent).unwrap_or_default(),
|
||||
"Cannot stake more than balance"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn check_delegators(
|
||||
delegations: BTreeMap<T::AccountId, Delegation<T>>,
|
||||
ledger: BTreeMap<T::AccountId, AgentLedger<T>>,
|
||||
) -> Result<(), pezsp_runtime::TryRuntimeError> {
|
||||
let mut delegation_aggregation = BTreeMap::<T::AccountId, BalanceOf<T>>::new();
|
||||
for (delegator, delegation) in delegations.iter() {
|
||||
ensure!(!Self::is_agent(delegator), "delegator cannot be an agent");
|
||||
|
||||
delegation_aggregation
|
||||
.entry(delegation.agent.clone())
|
||||
.and_modify(|e| *e += delegation.amount)
|
||||
.or_insert(delegation.amount);
|
||||
}
|
||||
|
||||
for (agent, total_delegated) in delegation_aggregation {
|
||||
ensure!(!Self::is_delegator(&agent), "agent cannot be delegator");
|
||||
|
||||
let ledger = ledger.get(&agent).expect("ledger should exist");
|
||||
ensure!(
|
||||
ledger.total_delegated == total_delegated,
|
||||
"ledger total delegated should match delegations"
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use super::*;
|
||||
use pezframe_support::traits::OnRuntimeUpgrade;
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use pezsp_runtime::TryRuntimeError;
|
||||
|
||||
pub mod unversioned {
|
||||
use super::*;
|
||||
#[cfg(feature = "try-runtime")]
|
||||
use alloc::vec::Vec;
|
||||
use pezsp_runtime::traits::AccountIdConversion;
|
||||
|
||||
/// Migrates `ProxyDelegator` accounts with better entropy than the old logic which didn't take
|
||||
/// into account all the bytes of the agent account ID.
|
||||
pub struct ProxyDelegatorMigration<T, MaxAgents>(PhantomData<(T, MaxAgents)>);
|
||||
|
||||
impl<T: Config, MaxAgents: Get<u32>> OnRuntimeUpgrade for ProxyDelegatorMigration<T, MaxAgents> {
|
||||
fn on_runtime_upgrade() -> Weight {
|
||||
let mut weight = Weight::zero();
|
||||
let old_proxy_delegator = |agent: T::AccountId| {
|
||||
T::PalletId::get()
|
||||
.into_sub_account_truncating((AccountType::ProxyDelegator, agent.clone()))
|
||||
};
|
||||
|
||||
Agents::<T>::iter_keys().take(MaxAgents::get() as usize).for_each(|agent| {
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 0));
|
||||
let old_proxy = old_proxy_delegator(agent.clone());
|
||||
|
||||
// if delegation does not exist, it does not need to be migrated.
|
||||
if let Some(delegation) = Delegation::<T>::get(&old_proxy) {
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 0));
|
||||
|
||||
let new_proxy =
|
||||
Pallet::<T>::generate_proxy_delegator(Agent::from(agent.clone()));
|
||||
|
||||
// accrue read writes for `do_migrate_delegation`
|
||||
weight.saturating_accrue(T::DbWeight::get().reads_writes(8, 8));
|
||||
let _ = Pallet::<T>::do_migrate_delegation(
|
||||
Delegator::from(old_proxy.clone()),
|
||||
new_proxy.clone(),
|
||||
delegation.amount,
|
||||
)
|
||||
.map_err(|e| {
|
||||
log!(
|
||||
error,
|
||||
"Failed to migrate old proxy delegator {:?} to new proxy {:?} for agent {:?} with error: {:?}",
|
||||
old_proxy,
|
||||
new_proxy,
|
||||
agent,
|
||||
e,
|
||||
);
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
log!(info, "Finished migrating old proxy delegator accounts to new ones");
|
||||
weight
|
||||
}
|
||||
|
||||
#[cfg(feature = "try-runtime")]
|
||||
fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
|
||||
let mut unmigrated_count = 0;
|
||||
let old_proxy_delegator = |agent: T::AccountId| {
|
||||
T::PalletId::get()
|
||||
.into_sub_account_truncating((AccountType::ProxyDelegator, agent.clone()))
|
||||
};
|
||||
|
||||
Agents::<T>::iter_keys().take(MaxAgents::get() as usize).for_each(|agent| {
|
||||
let old_proxy: T::AccountId = old_proxy_delegator(agent.clone());
|
||||
let held_balance = Pallet::<T>::held_balance_of(Delegator::from(old_proxy.clone()));
|
||||
let delegation = Delegation::<T>::get(&old_proxy);
|
||||
if delegation.is_some() || !held_balance.is_zero() {
|
||||
log!(
|
||||
error,
|
||||
"Old proxy delegator {:?} for agent {:?} is not migrated.",
|
||||
old_proxy,
|
||||
agent,
|
||||
);
|
||||
unmigrated_count += 1;
|
||||
}
|
||||
});
|
||||
|
||||
if unmigrated_count > 0 {
|
||||
Err(TryRuntimeError::Other("Some old proxy delegator accounts are not migrated."))
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::{self as delegated_staking, types::AgentLedgerOuter};
|
||||
use pezframe_support::{
|
||||
assert_ok, derive_impl,
|
||||
pezpallet_prelude::*,
|
||||
parameter_types,
|
||||
traits::{ConstU64, Currency, VariantCountOf},
|
||||
PalletId,
|
||||
};
|
||||
|
||||
use pezsp_runtime::{traits::IdentityLookup, BuildStorage, Perbill};
|
||||
|
||||
use pezframe_election_provider_support::{
|
||||
bounds::{ElectionBounds, ElectionBoundsBuilder},
|
||||
onchain, SequentialPhragmen,
|
||||
};
|
||||
use pezframe_support::dispatch::RawOrigin;
|
||||
use pezpallet_staking::{ActiveEra, ActiveEraInfo, CurrentEra};
|
||||
use pezsp_core::{ConstBool, U256};
|
||||
use pezsp_runtime::traits::Convert;
|
||||
use pezsp_staking::{Agent, Stake, StakingInterface};
|
||||
|
||||
pub type T = Runtime;
|
||||
type Block = pezframe_system::mocking::MockBlock<Runtime>;
|
||||
pub type AccountId = u128;
|
||||
|
||||
pub const GENESIS_VALIDATOR: AccountId = 1;
|
||||
pub const GENESIS_NOMINATOR_ONE: AccountId = 101;
|
||||
pub const GENESIS_NOMINATOR_TWO: AccountId = 102;
|
||||
|
||||
#[derive_impl(pezframe_system::config_preludes::TestDefaultConfig)]
|
||||
impl pezframe_system::Config for Runtime {
|
||||
type Block = Block;
|
||||
type AccountData = pezpallet_balances::AccountData<Balance>;
|
||||
type AccountId = AccountId;
|
||||
type Lookup = IdentityLookup<Self::AccountId>;
|
||||
}
|
||||
|
||||
impl pezpallet_timestamp::Config for Runtime {
|
||||
type Moment = u64;
|
||||
type OnTimestampSet = ();
|
||||
type MinimumPeriod = ConstU64<5>;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
pub type Balance = u128;
|
||||
|
||||
parameter_types! {
|
||||
pub static ExistentialDeposit: Balance = 1;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_balances::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_balances::Config for Runtime {
|
||||
type Balance = Balance;
|
||||
type ExistentialDeposit = ExistentialDeposit;
|
||||
type AccountStore = System;
|
||||
type FreezeIdentifier = RuntimeFreezeReason;
|
||||
type MaxFreezes = VariantCountOf<RuntimeFreezeReason>;
|
||||
type RuntimeFreezeReason = RuntimeFreezeReason;
|
||||
}
|
||||
|
||||
pezpallet_staking_reward_curve::build! {
|
||||
const I_NPOS: pezsp_runtime::curve::PiecewiseLinear<'static> = curve!(
|
||||
min_inflation: 0_025_000,
|
||||
max_inflation: 0_100_000,
|
||||
ideal_stake: 0_500_000,
|
||||
falloff: 0_050_000,
|
||||
max_piece_count: 40,
|
||||
test_precision: 0_005_000,
|
||||
);
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const RewardCurve: &'static pezsp_runtime::curve::PiecewiseLinear<'static> = &I_NPOS;
|
||||
pub static ElectionsBoundsOnChain: ElectionBounds = ElectionBoundsBuilder::default().build();
|
||||
}
|
||||
pub struct OnChainSeqPhragmen;
|
||||
impl onchain::Config for OnChainSeqPhragmen {
|
||||
type System = Runtime;
|
||||
type Solver = SequentialPhragmen<Balance, pezsp_runtime::Perbill>;
|
||||
type DataProvider = Staking;
|
||||
type WeightInfo = ();
|
||||
type MaxWinnersPerPage = ConstU32<100>;
|
||||
type MaxBackersPerWinner = ConstU32<100>;
|
||||
type Sort = ConstBool<true>;
|
||||
type Bounds = ElectionsBoundsOnChain;
|
||||
}
|
||||
|
||||
#[derive_impl(pezpallet_staking::config_preludes::TestDefaultConfig)]
|
||||
impl pezpallet_staking::Config for Runtime {
|
||||
type OldCurrency = Balances;
|
||||
type Currency = Balances;
|
||||
type UnixTime = pezpallet_timestamp::Pallet<Self>;
|
||||
type AdminOrigin = pezframe_system::EnsureRoot<Self::AccountId>;
|
||||
type EraPayout = pezpallet_staking::ConvertCurve<RewardCurve>;
|
||||
type ElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = Self::ElectionProvider;
|
||||
type VoterList = pezpallet_staking::UseNominatorsAndValidatorsMap<Self>;
|
||||
type TargetList = pezpallet_staking::UseValidatorsMap<Self>;
|
||||
type EventListeners = (Pools, DelegatedStaking);
|
||||
type Filter = pezpallet_nomination_pools::AllPoolMembers<Self>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub const DelegatedStakingPalletId: PalletId = PalletId(*b"py/dlstk");
|
||||
pub const SlashRewardFraction: Perbill = Perbill::from_percent(10);
|
||||
}
|
||||
impl delegated_staking::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type PalletId = DelegatedStakingPalletId;
|
||||
type Currency = Balances;
|
||||
type OnSlash = ();
|
||||
type SlashRewardFraction = SlashRewardFraction;
|
||||
type RuntimeHoldReason = RuntimeHoldReason;
|
||||
type CoreStaking = Staking;
|
||||
}
|
||||
|
||||
pub struct BalanceToU256;
|
||||
impl Convert<Balance, U256> for BalanceToU256 {
|
||||
fn convert(n: Balance) -> U256 {
|
||||
n.into()
|
||||
}
|
||||
}
|
||||
pub struct U256ToBalance;
|
||||
impl Convert<U256, Balance> for U256ToBalance {
|
||||
fn convert(n: U256) -> Balance {
|
||||
n.try_into().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub static MaxUnbonding: u32 = 8;
|
||||
pub const PoolsPalletId: PalletId = PalletId(*b"py/nopls");
|
||||
}
|
||||
impl pezpallet_nomination_pools::Config for Runtime {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type WeightInfo = ();
|
||||
type Currency = Balances;
|
||||
type RuntimeFreezeReason = RuntimeFreezeReason;
|
||||
type RewardCounter = pezsp_runtime::FixedU128;
|
||||
type BalanceToU256 = BalanceToU256;
|
||||
type U256ToBalance = U256ToBalance;
|
||||
type PostUnbondingPoolsWindow = ConstU32<2>;
|
||||
type PalletId = PoolsPalletId;
|
||||
type MaxMetadataLen = ConstU32<256>;
|
||||
type MaxUnbonding = MaxUnbonding;
|
||||
type MaxPointsToBalance = pezframe_support::traits::ConstU8<10>;
|
||||
type StakeAdapter =
|
||||
pezpallet_nomination_pools::adapter::DelegateStake<Self, Staking, DelegatedStaking>;
|
||||
type AdminOrigin = pezframe_system::EnsureRoot<Self::AccountId>;
|
||||
type BlockNumberProvider = System;
|
||||
type Filter = pezpallet_staking::AllStakers<Runtime>;
|
||||
}
|
||||
|
||||
pezframe_support::construct_runtime!(
|
||||
pub enum Runtime {
|
||||
System: pezframe_system,
|
||||
Timestamp: pezpallet_timestamp,
|
||||
Balances: pezpallet_balances,
|
||||
Staking: pezpallet_staking,
|
||||
Pools: pezpallet_nomination_pools,
|
||||
DelegatedStaking: delegated_staking,
|
||||
}
|
||||
);
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ExtBuilder {}
|
||||
|
||||
impl ExtBuilder {
|
||||
fn build(self) -> pezsp_io::TestExternalities {
|
||||
pezsp_tracing::try_init_simple();
|
||||
let mut storage =
|
||||
pezframe_system::GenesisConfig::<Runtime>::default().build_storage().unwrap();
|
||||
|
||||
let _ = pezpallet_balances::GenesisConfig::<T> {
|
||||
balances: vec![
|
||||
(GENESIS_VALIDATOR, 10000),
|
||||
(GENESIS_NOMINATOR_ONE, 1000),
|
||||
(GENESIS_NOMINATOR_TWO, 2000),
|
||||
],
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut storage);
|
||||
|
||||
let stakers = vec![
|
||||
(
|
||||
GENESIS_VALIDATOR,
|
||||
GENESIS_VALIDATOR,
|
||||
1000,
|
||||
pezsp_staking::StakerStatus::<AccountId>::Validator,
|
||||
),
|
||||
(
|
||||
GENESIS_NOMINATOR_ONE,
|
||||
GENESIS_NOMINATOR_ONE,
|
||||
100,
|
||||
pezsp_staking::StakerStatus::<AccountId>::Nominator(vec![1]),
|
||||
),
|
||||
(
|
||||
GENESIS_NOMINATOR_TWO,
|
||||
GENESIS_NOMINATOR_TWO,
|
||||
200,
|
||||
pezsp_staking::StakerStatus::<AccountId>::Nominator(vec![1]),
|
||||
),
|
||||
];
|
||||
|
||||
let _ = pezpallet_staking::GenesisConfig::<T> {
|
||||
stakers: stakers.clone(),
|
||||
// ideal validator count
|
||||
validator_count: 2,
|
||||
minimum_validator_count: 1,
|
||||
invulnerables: vec![],
|
||||
slash_reward_fraction: Perbill::from_percent(10),
|
||||
min_nominator_bond: ExistentialDeposit::get(),
|
||||
min_validator_bond: ExistentialDeposit::get(),
|
||||
..Default::default()
|
||||
}
|
||||
.assimilate_storage(&mut storage);
|
||||
|
||||
let mut ext = pezsp_io::TestExternalities::from(storage);
|
||||
|
||||
ext.execute_with(|| {
|
||||
// for events to be deposited.
|
||||
pezframe_system::Pallet::<Runtime>::set_block_number(1);
|
||||
// set era for staking.
|
||||
start_era(0);
|
||||
});
|
||||
|
||||
ext
|
||||
}
|
||||
pub fn build_and_execute(self, test: impl FnOnce()) {
|
||||
pezsp_tracing::try_init_simple();
|
||||
let mut ext = self.build();
|
||||
ext.execute_with(test);
|
||||
ext.execute_with(|| {
|
||||
#[cfg(feature = "try-runtime")]
|
||||
<AllPalletsWithSystem as pezframe_support::traits::TryState<u64>>::try_state(
|
||||
pezframe_system::Pallet::<Runtime>::block_number(),
|
||||
pezframe_support::traits::TryStateSelect::All,
|
||||
)
|
||||
.unwrap();
|
||||
#[cfg(not(feature = "try-runtime"))]
|
||||
DelegatedStaking::do_try_state().unwrap();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// fund and return who.
|
||||
pub(crate) fn fund(who: &AccountId, amount: Balance) {
|
||||
let _ = Balances::deposit_creating(who, amount);
|
||||
}
|
||||
|
||||
/// Sets up delegation for passed delegators, returns total delegated amount.
|
||||
///
|
||||
/// `delegate_amount` is incremented by the amount `increment` starting with `base_delegate_amount`
|
||||
/// from lower index to higher index of delegators.
|
||||
pub(crate) fn setup_delegation_stake(
|
||||
agent: AccountId,
|
||||
reward_acc: AccountId,
|
||||
delegators: Vec<AccountId>,
|
||||
base_delegate_amount: Balance,
|
||||
increment: Balance,
|
||||
) -> Balance {
|
||||
fund(&agent, 100);
|
||||
assert_ok!(DelegatedStaking::register_agent(RawOrigin::Signed(agent).into(), reward_acc));
|
||||
let mut delegated_amount: Balance = 0;
|
||||
for (index, delegator) in delegators.iter().enumerate() {
|
||||
let amount_to_delegate = base_delegate_amount + increment * index as Balance;
|
||||
delegated_amount += amount_to_delegate;
|
||||
|
||||
fund(delegator, amount_to_delegate + ExistentialDeposit::get());
|
||||
assert_ok!(DelegatedStaking::delegate_to_agent(
|
||||
RawOrigin::Signed(*delegator).into(),
|
||||
agent,
|
||||
amount_to_delegate
|
||||
));
|
||||
}
|
||||
|
||||
// sanity checks
|
||||
assert_eq!(DelegatedStaking::stakeable_balance(Agent::from(agent)), delegated_amount);
|
||||
assert_eq!(AgentLedgerOuter::<T>::get(&agent).unwrap().available_to_bond(), 0);
|
||||
|
||||
delegated_amount
|
||||
}
|
||||
|
||||
pub(crate) fn start_era(era: pezsp_staking::EraIndex) {
|
||||
CurrentEra::<T>::set(Some(era));
|
||||
ActiveEra::<T>::set(Some(ActiveEraInfo { index: era, start: None }));
|
||||
}
|
||||
|
||||
pub(crate) fn eq_stake(who: AccountId, total: Balance, active: Balance) -> bool {
|
||||
Staking::stake(&who).unwrap() == Stake { total, active } &&
|
||||
get_agent_ledger(&who).ledger.stakeable_balance() == total
|
||||
}
|
||||
|
||||
pub(crate) fn get_agent_ledger(agent: &AccountId) -> AgentLedgerOuter<T> {
|
||||
AgentLedgerOuter::<T>::get(agent).expect("delegate should exist")
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
static ObservedEventsDelegatedStaking: usize = 0;
|
||||
static ObservedEventsPools: usize = 0;
|
||||
}
|
||||
|
||||
pub(crate) fn pool_events_since_last_call() -> Vec<pezpallet_nomination_pools::Event<Runtime>> {
|
||||
let events = System::read_events_for_pallet::<pezpallet_nomination_pools::Event<Runtime>>();
|
||||
let already_seen = ObservedEventsPools::get();
|
||||
ObservedEventsPools::set(events.len());
|
||||
events.into_iter().skip(already_seen).collect()
|
||||
}
|
||||
|
||||
pub(crate) fn events_since_last_call() -> Vec<crate::Event<Runtime>> {
|
||||
let events = System::read_events_for_pallet::<crate::Event<Runtime>>();
|
||||
let already_seen = ObservedEventsDelegatedStaking::get();
|
||||
ObservedEventsDelegatedStaking::set(events.len());
|
||||
events.into_iter().skip(already_seen).collect()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,296 @@
|
||||
// This file is part of Bizinikiwi.
|
||||
|
||||
// Copyright (C) Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Basic types used in delegated staking.
|
||||
|
||||
use super::*;
|
||||
use pezframe_support::traits::DefensiveSaturating;
|
||||
|
||||
/// The type of pot account being created.
|
||||
#[derive(Encode, Decode)]
|
||||
pub(crate) enum AccountType {
|
||||
/// A proxy delegator account created for a nominator who migrated to an `Agent` account.
|
||||
///
|
||||
/// Funds for unmigrated `delegator` accounts of the `Agent` are kept here.
|
||||
ProxyDelegator,
|
||||
}
|
||||
|
||||
/// Information about delegation of a `delegator`.
|
||||
#[derive(Default, Encode, Clone, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct Delegation<T: Config> {
|
||||
/// The target of delegation.
|
||||
pub agent: T::AccountId,
|
||||
/// The amount delegated.
|
||||
pub amount: BalanceOf<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> Delegation<T> {
|
||||
/// Get delegation of a `delegator`.
|
||||
pub(crate) fn get(delegator: &T::AccountId) -> Option<Self> {
|
||||
<Delegators<T>>::get(delegator)
|
||||
}
|
||||
|
||||
/// Create and return a new delegation instance.
|
||||
pub(crate) fn new(agent: &T::AccountId, amount: BalanceOf<T>) -> Self {
|
||||
Delegation { agent: agent.clone(), amount }
|
||||
}
|
||||
|
||||
/// Ensure the delegator is either a new delegator or they are adding more delegation to the
|
||||
/// existing agent.
|
||||
///
|
||||
/// Delegators are prevented from delegating to multiple agents at the same time.
|
||||
pub(crate) fn can_delegate(delegator: &T::AccountId, agent: &T::AccountId) -> bool {
|
||||
Delegation::<T>::get(delegator)
|
||||
.map(|delegation| delegation.agent == *agent)
|
||||
.unwrap_or(
|
||||
// all good if it is a new delegator except it should not be an existing agent.
|
||||
!<Agents<T>>::contains_key(delegator),
|
||||
)
|
||||
}
|
||||
|
||||
/// Save self to storage.
|
||||
///
|
||||
/// If the delegation amount is zero, remove the delegation. Also adds and removes provider
|
||||
/// reference as needed.
|
||||
pub(crate) fn update(self, key: &T::AccountId) {
|
||||
if <Delegators<T>>::contains_key(key) {
|
||||
// Clean up if no delegation left.
|
||||
if self.amount == Zero::zero() {
|
||||
<Delegators<T>>::remove(key);
|
||||
// Remove provider if no delegation left.
|
||||
let _ = pezframe_system::Pallet::<T>::dec_providers(key).defensive();
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// this is a new delegation. Provide for this account.
|
||||
pezframe_system::Pallet::<T>::inc_providers(key);
|
||||
}
|
||||
|
||||
<Delegators<T>>::insert(key, self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ledger of all delegations to an `Agent`.
|
||||
///
|
||||
/// This keeps track of the active balance of the `Agent` that is made up from the funds that
|
||||
/// are currently delegated to this `Agent`. It also tracks the pending slashes yet to be
|
||||
/// applied among other things.
|
||||
#[derive(Default, Clone, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct AgentLedger<T: Config> {
|
||||
/// Where the reward should be paid out.
|
||||
pub payee: T::AccountId,
|
||||
/// Sum of all delegated funds to this `Agent`.
|
||||
#[codec(compact)]
|
||||
pub total_delegated: BalanceOf<T>,
|
||||
/// Funds that are withdrawn from core staking but not released to delegator/s. It is a subset
|
||||
/// of `total_delegated` and can never be greater than it.
|
||||
///
|
||||
/// We need this register to ensure that the `Agent` does not bond funds from delegated
|
||||
/// funds that are withdrawn and should be claimed by delegators.
|
||||
#[codec(compact)]
|
||||
pub unclaimed_withdrawals: BalanceOf<T>,
|
||||
/// Slashes that are not yet applied. This affects the effective balance of the `Agent`.
|
||||
#[codec(compact)]
|
||||
pub pending_slash: BalanceOf<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> AgentLedger<T> {
|
||||
/// Create a new instance of `AgentLedger`.
|
||||
pub(crate) fn new(reward_destination: &T::AccountId) -> Self {
|
||||
AgentLedger {
|
||||
payee: reward_destination.clone(),
|
||||
total_delegated: Zero::zero(),
|
||||
unclaimed_withdrawals: Zero::zero(),
|
||||
pending_slash: Zero::zero(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get `AgentLedger` from storage.
|
||||
pub(crate) fn get(key: &T::AccountId) -> Option<Self> {
|
||||
<Agents<T>>::get(key)
|
||||
}
|
||||
|
||||
/// Save self to storage with the given key.
|
||||
///
|
||||
/// Increments provider count if this is a new agent.
|
||||
pub(crate) fn update(self, key: &T::AccountId) {
|
||||
<Agents<T>>::insert(key, self)
|
||||
}
|
||||
|
||||
/// Remove self from storage.
|
||||
pub(crate) fn remove(key: &T::AccountId) {
|
||||
debug_assert!(<Agents<T>>::contains_key(key), "Agent should exist in storage");
|
||||
<Agents<T>>::remove(key);
|
||||
}
|
||||
|
||||
/// Effective total balance of the `Agent`.
|
||||
///
|
||||
/// This takes into account any slashes reported to `Agent` but unapplied.
|
||||
pub(crate) fn effective_balance(&self) -> BalanceOf<T> {
|
||||
defensive_assert!(
|
||||
self.total_delegated >= self.pending_slash,
|
||||
"slash cannot be higher than actual balance of delegator"
|
||||
);
|
||||
|
||||
// pending slash needs to be burned and cannot be used for stake.
|
||||
self.total_delegated.saturating_sub(self.pending_slash)
|
||||
}
|
||||
|
||||
/// Agent balance that can be staked/bonded in [`T::CoreStaking`].
|
||||
pub(crate) fn stakeable_balance(&self) -> BalanceOf<T> {
|
||||
self.effective_balance().saturating_sub(self.unclaimed_withdrawals)
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrapper around `AgentLedger` to provide some helper functions to mutate the ledger.
|
||||
#[derive(Clone)]
|
||||
pub struct AgentLedgerOuter<T: Config> {
|
||||
/// storage key
|
||||
pub key: T::AccountId,
|
||||
/// storage value
|
||||
pub ledger: AgentLedger<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> AgentLedgerOuter<T> {
|
||||
/// Get `Agent` from storage if it exists or return an error.
|
||||
pub(crate) fn get(agent: &T::AccountId) -> Result<AgentLedgerOuter<T>, DispatchError> {
|
||||
let ledger = AgentLedger::<T>::get(agent).ok_or(Error::<T>::NotAgent)?;
|
||||
Ok(AgentLedgerOuter { key: agent.clone(), ledger })
|
||||
}
|
||||
|
||||
/// Remove funds that are withdrawn from [Config::CoreStaking] but not claimed by a delegator.
|
||||
///
|
||||
/// Checked decrease of delegation amount from `total_delegated` and `unclaimed_withdrawals`
|
||||
/// registers. Consumes self and returns a new instance of self if success.
|
||||
pub(crate) fn remove_unclaimed_withdraw(
|
||||
self,
|
||||
amount: BalanceOf<T>,
|
||||
) -> Result<Self, DispatchError> {
|
||||
let new_total_delegated = self
|
||||
.ledger
|
||||
.total_delegated
|
||||
.checked_sub(&amount)
|
||||
.defensive_ok_or(ArithmeticError::Overflow)?;
|
||||
let new_unclaimed_withdrawals = self
|
||||
.ledger
|
||||
.unclaimed_withdrawals
|
||||
.checked_sub(&amount)
|
||||
.defensive_ok_or(ArithmeticError::Overflow)?;
|
||||
|
||||
Ok(AgentLedgerOuter {
|
||||
ledger: AgentLedger {
|
||||
total_delegated: new_total_delegated,
|
||||
unclaimed_withdrawals: new_unclaimed_withdrawals,
|
||||
..self.ledger
|
||||
},
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
/// Add funds that are withdrawn from [Config::CoreStaking] to be claimed by delegators later.
|
||||
pub(crate) fn add_unclaimed_withdraw(
|
||||
self,
|
||||
amount: BalanceOf<T>,
|
||||
) -> Result<Self, DispatchError> {
|
||||
let new_unclaimed_withdrawals = self
|
||||
.ledger
|
||||
.unclaimed_withdrawals
|
||||
.checked_add(&amount)
|
||||
.defensive_ok_or(ArithmeticError::Overflow)?;
|
||||
|
||||
Ok(AgentLedgerOuter {
|
||||
ledger: AgentLedger { unclaimed_withdrawals: new_unclaimed_withdrawals, ..self.ledger },
|
||||
..self
|
||||
})
|
||||
}
|
||||
|
||||
/// Amount that is delegated but not bonded yet.
|
||||
///
|
||||
/// This importantly does not include `unclaimed_withdrawals` as those should not be bonded
|
||||
/// again unless explicitly requested.
|
||||
pub(crate) fn available_to_bond(&self) -> BalanceOf<T> {
|
||||
let bonded_stake = self.bonded_stake();
|
||||
let stakeable = self.ledger.stakeable_balance();
|
||||
|
||||
defensive_assert!(
|
||||
stakeable >= bonded_stake,
|
||||
"cannot be bonded with more than total amount delegated to agent"
|
||||
);
|
||||
|
||||
stakeable.saturating_sub(bonded_stake)
|
||||
}
|
||||
|
||||
/// Remove slashes from the `AgentLedger`.
|
||||
pub(crate) fn remove_slash(self, amount: BalanceOf<T>) -> Self {
|
||||
let pending_slash = self.ledger.pending_slash.defensive_saturating_sub(amount);
|
||||
let total_delegated = self.ledger.total_delegated.defensive_saturating_sub(amount);
|
||||
|
||||
AgentLedgerOuter {
|
||||
ledger: AgentLedger { pending_slash, total_delegated, ..self.ledger },
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the total stake of agent bonded in [`Config::CoreStaking`].
|
||||
pub(crate) fn bonded_stake(&self) -> BalanceOf<T> {
|
||||
T::CoreStaking::total_stake(&self.key).unwrap_or(Zero::zero())
|
||||
}
|
||||
|
||||
/// Returns true if the agent is bonded in [`Config::CoreStaking`].
|
||||
pub(crate) fn is_bonded(&self) -> bool {
|
||||
T::CoreStaking::stake(&self.key).is_ok()
|
||||
}
|
||||
|
||||
/// Returns the reward account registered by the agent.
|
||||
pub(crate) fn reward_account(&self) -> &T::AccountId {
|
||||
&self.ledger.payee
|
||||
}
|
||||
|
||||
/// Save self to storage.
|
||||
pub(crate) fn save(self) {
|
||||
let key = self.key;
|
||||
self.ledger.update(&key)
|
||||
}
|
||||
|
||||
/// Update agent ledger.
|
||||
pub(crate) fn update(self) {
|
||||
let key = self.key;
|
||||
self.ledger.update(&key);
|
||||
}
|
||||
|
||||
/// Reloads self from storage.
|
||||
pub(crate) fn reload(self) -> Result<AgentLedgerOuter<T>, DispatchError> {
|
||||
Self::get(&self.key)
|
||||
}
|
||||
|
||||
/// Balance of `Agent` that is not bonded.
|
||||
///
|
||||
/// This is similar to [Self::available_to_bond] except it also includes `unclaimed_withdrawals`
|
||||
/// of `Agent`.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn total_unbonded(&self) -> BalanceOf<T> {
|
||||
let bonded_stake = self.bonded_stake();
|
||||
|
||||
let net_balance = self.ledger.effective_balance();
|
||||
|
||||
assert!(net_balance >= bonded_stake, "cannot be bonded with more than the agent balance");
|
||||
|
||||
net_balance.saturating_sub(bonded_stake)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user