mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-16 21:21:03 +00:00
Non-Interactive Staking (#12610)
* Improve naming. * More improvements to naming * Fungible counterpart * Shared pot instead of reserve * Transferable receipts * Better naming * Use u128 for counterpart * Partial thawing * Docs * Remove AdminOrigin * Integrate into Kitchen Sink * Thaw throttling * Remove todo * Docs * Fix benchmarks * Building * Tests work * New benchmarks * Benchmarking tests * Test new defensive_saturating_* functions Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * fmt Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Formatting * Update frame/nis/src/lib.rs Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Apply suggestions from code review Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Events added * Fix kitchensink * Update frame/nis/src/lib.rs Co-authored-by: Xiliang Chen <xlchen1291@gmail.com> * Review niggles * Remove genesis build requirements * Grumbles * Fixes * Fixes * Fixes * Update frame/nis/src/lib.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Update primitives/runtime/src/traits.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * Formatting * Fixes * Fix node genesis config Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Fix node chain specs Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Use free asset ID as counterpart Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Account for rounding errors in fund_deficit bench Relaxes the check for the NIS account balance in the fund_deficit bench from equality from to checking for 99.999% equality. The exact deviation for the kitchensink runtime config is 1.24e-10 percent but could vary if the config is changed. Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * clippy Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * fmt Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> * Fix * Rename * Fixes * Fixes * Formatting Signed-off-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Oliver Tale-Yazdi <oliver.tale-yazdi@parity.io> Co-authored-by: Xiliang Chen <xlchen1291@gmail.com> Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,936 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2019-2022 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.
|
||||
|
||||
//! # Non-Interactive Staking (NIS) Pallet
|
||||
//! A pallet allowing accounts to auction for being frozen and receive open-ended
|
||||
//! inflation-protection in return.
|
||||
//!
|
||||
//! ## Overview
|
||||
//!
|
||||
//! Lock up tokens, for at least as long as you offer, and be free from both inflation and
|
||||
//! intermediate reward or exchange until the tokens become unlocked.
|
||||
//!
|
||||
//! ## Design
|
||||
//!
|
||||
//! Queues for each of `1..QueueCount` periods, given in blocks (`Period`). Queues are limited in
|
||||
//! size to something sensible, `MaxQueueLen`. A secondary storage item with `QueueCount` x `u32`
|
||||
//! elements with the number of items in each queue.
|
||||
//!
|
||||
//! Queues are split into two parts. The first part is a priority queue based on bid size. The
|
||||
//! second part is just a FIFO (the size of the second part is set with `FifoQueueLen`). Items are
|
||||
//! always prepended so that removal is always O(1) since removal often happens many times under a
|
||||
//! single weighed function (`on_initialize`) yet placing bids only ever happens once per weighed
|
||||
//! function (`place_bid`). If the queue has a priority portion, then it remains sorted in order of
|
||||
//! bid size so that smaller bids fall off as it gets too large.
|
||||
//!
|
||||
//! Account may enqueue a balance with some number of `Period`s lock up, up to a maximum of
|
||||
//! `QueueCount`. The balance gets reserved. There's a minimum of `MinBid` to avoid dust.
|
||||
//!
|
||||
//! Until your bid is consolidated and you receive a receipt, you can retract it instantly and the
|
||||
//! funds are unreserved.
|
||||
//!
|
||||
//! There's a target proportion of effective total issuance (i.e. accounting for existing receipts)
|
||||
//! which the pallet attempts to have frozen at any one time. It will likely be gradually increased
|
||||
//! over time by governance.
|
||||
//!
|
||||
//! As the proportion of effective total issuance represented by outstanding receipts drops below
|
||||
//! `FrozenFraction`, then bids are taken from queues and consolidated into receipts, with the queue
|
||||
//! of the greatest period taking priority. If the item in the queue's locked amount is greater than
|
||||
//! the amount remaining to achieve `FrozenFraction`, then it is split up into multiple bids and
|
||||
//! becomes partially consolidated.
|
||||
//!
|
||||
//! With the consolidation of a bid, the bid amount is taken from the owner and a receipt is issued.
|
||||
//! The receipt records the proportion of the bid compared to effective total issuance at the time
|
||||
//! of consolidation. The receipt has two independent elements: a "main" non-fungible receipt and
|
||||
//! a second set of fungible "counterpart" tokens. The accounting functionality of the latter must
|
||||
//! be provided through the `Counterpart` trait item. The main non-fungible receipt may have its
|
||||
//! owner transferred through the pallet's implementation of `nonfungible::Transfer`.
|
||||
//!
|
||||
//! A later `thaw` function may be called in order to reduce the recorded proportion or entirely
|
||||
//! remove the receipt in return for the appropriate proportion of the effective total issuance.
|
||||
//! This may happen no earlier than queue's period after the point at which the receipt was issued.
|
||||
//! The call must be made by the owner of both the "main" non-fungible receipt and the appropriate
|
||||
//! amount of counterpart tokens.
|
||||
//!
|
||||
//! `NoCounterpart` may be provided as an implementation for the counterpart token system in which
|
||||
//! case they are completely disregarded from the thawing logic.
|
||||
//!
|
||||
//! ## Terms
|
||||
//!
|
||||
//! - *Effective total issuance*: The total issuance of balances in the system, including all claims
|
||||
//! of all outstanding receipts but excluding `IgnoredIssuance`.
|
||||
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
|
||||
use frame_support::{
|
||||
dispatch::{DispatchError, DispatchResult},
|
||||
traits::fungible::{Inspect as FungibleInspect, Mutate as FungibleMutate},
|
||||
};
|
||||
pub use pallet::*;
|
||||
use sp_arithmetic::{traits::Unsigned, RationalArg};
|
||||
use sp_core::TypedGet;
|
||||
use sp_runtime::{
|
||||
traits::{Convert, ConvertBack},
|
||||
Perquintill,
|
||||
};
|
||||
|
||||
mod benchmarking;
|
||||
#[cfg(test)]
|
||||
mod mock;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
pub mod weights;
|
||||
|
||||
pub struct WithMaximumOf<A: TypedGet>(sp_std::marker::PhantomData<A>);
|
||||
impl<A: TypedGet> Convert<Perquintill, A::Type> for WithMaximumOf<A>
|
||||
where
|
||||
A::Type: Clone + Unsigned + From<u64>,
|
||||
u64: TryFrom<A::Type>,
|
||||
{
|
||||
fn convert(a: Perquintill) -> A::Type {
|
||||
a * A::get()
|
||||
}
|
||||
}
|
||||
impl<A: TypedGet> ConvertBack<Perquintill, A::Type> for WithMaximumOf<A>
|
||||
where
|
||||
A::Type: RationalArg + From<u64>,
|
||||
u64: TryFrom<A::Type>,
|
||||
u128: TryFrom<A::Type>,
|
||||
{
|
||||
fn convert_back(a: A::Type) -> Perquintill {
|
||||
Perquintill::from_rational(a, A::get())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NoCounterpart<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T> FungibleInspect<T> for NoCounterpart<T> {
|
||||
type Balance = u32;
|
||||
fn total_issuance() -> u32 {
|
||||
0
|
||||
}
|
||||
fn minimum_balance() -> u32 {
|
||||
0
|
||||
}
|
||||
fn balance(_who: &T) -> u32 {
|
||||
0
|
||||
}
|
||||
fn reducible_balance(_who: &T, _keep_alive: bool) -> u32 {
|
||||
0
|
||||
}
|
||||
fn can_deposit(
|
||||
_who: &T,
|
||||
_amount: u32,
|
||||
_mint: bool,
|
||||
) -> frame_support::traits::tokens::DepositConsequence {
|
||||
frame_support::traits::tokens::DepositConsequence::Success
|
||||
}
|
||||
fn can_withdraw(
|
||||
_who: &T,
|
||||
_amount: u32,
|
||||
) -> frame_support::traits::tokens::WithdrawConsequence<u32> {
|
||||
frame_support::traits::tokens::WithdrawConsequence::Success
|
||||
}
|
||||
}
|
||||
impl<T> FungibleMutate<T> for NoCounterpart<T> {
|
||||
fn mint_into(_who: &T, _amount: u32) -> DispatchResult {
|
||||
Ok(())
|
||||
}
|
||||
fn burn_from(_who: &T, _amount: u32) -> Result<u32, DispatchError> {
|
||||
Ok(0)
|
||||
}
|
||||
}
|
||||
impl<T> Convert<Perquintill, u32> for NoCounterpart<T> {
|
||||
fn convert(_: Perquintill) -> u32 {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::{FungibleInspect, FungibleMutate};
|
||||
pub use crate::weights::WeightInfo;
|
||||
use frame_support::{
|
||||
pallet_prelude::*,
|
||||
traits::{
|
||||
nonfungible::{Inspect as NonfungibleInspect, Transfer as NonfungibleTransfer},
|
||||
Currency, Defensive, DefensiveSaturating,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
OnUnbalanced, ReservableCurrency,
|
||||
},
|
||||
PalletId,
|
||||
};
|
||||
use frame_system::pallet_prelude::*;
|
||||
use sp_arithmetic::{PerThing, Perquintill};
|
||||
use sp_runtime::{
|
||||
traits::{AccountIdConversion, Bounded, Convert, ConvertBack, Saturating, Zero},
|
||||
TokenError,
|
||||
};
|
||||
use sp_std::prelude::*;
|
||||
|
||||
type BalanceOf<T> =
|
||||
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
|
||||
type PositiveImbalanceOf<T> = <<T as Config>::Currency as Currency<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
>>::PositiveImbalance;
|
||||
type ReceiptRecordOf<T> = ReceiptRecord<
|
||||
<T as frame_system::Config>::AccountId,
|
||||
<T as frame_system::Config>::BlockNumber,
|
||||
>;
|
||||
type IssuanceInfoOf<T> = IssuanceInfo<BalanceOf<T>>;
|
||||
type SummaryRecordOf<T> = SummaryRecord<<T as frame_system::Config>::BlockNumber>;
|
||||
type BidOf<T> = Bid<BalanceOf<T>, <T as frame_system::Config>::AccountId>;
|
||||
type QueueTotalsTypeOf<T> = BoundedVec<(u32, BalanceOf<T>), <T as Config>::QueueCount>;
|
||||
|
||||
#[pallet::config]
|
||||
pub trait Config: frame_system::Config {
|
||||
/// Information on runtime weights.
|
||||
type WeightInfo: WeightInfo;
|
||||
|
||||
/// Overarching event type.
|
||||
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
|
||||
|
||||
/// The treasury's pallet id, used for deriving its sovereign account ID.
|
||||
#[pallet::constant]
|
||||
type PalletId: Get<PalletId>;
|
||||
|
||||
/// Currency type that this works on.
|
||||
type Currency: ReservableCurrency<Self::AccountId, Balance = Self::CurrencyBalance>;
|
||||
|
||||
/// Just the `Currency::Balance` type; we have this item to allow us to constrain it to
|
||||
/// `From<u64>`.
|
||||
type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned
|
||||
+ codec::FullCodec
|
||||
+ Copy
|
||||
+ MaybeSerializeDeserialize
|
||||
+ sp_std::fmt::Debug
|
||||
+ Default
|
||||
+ From<u64>
|
||||
+ TypeInfo
|
||||
+ MaxEncodedLen;
|
||||
|
||||
/// Origin required for auto-funding the deficit.
|
||||
type FundOrigin: EnsureOrigin<Self::RuntimeOrigin>;
|
||||
|
||||
/// The issuance to ignore. This is subtracted from the `Currency`'s `total_issuance` to get
|
||||
/// the issuance with which we determine the thawed value of a given proportion.
|
||||
type IgnoredIssuance: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The accounting system for the fungible counterpart tokens.
|
||||
type Counterpart: FungibleMutate<Self::AccountId>;
|
||||
|
||||
/// The system to convert an overall proportion of issuance into a number of fungible
|
||||
/// counterpart tokens.
|
||||
///
|
||||
/// In general it's best to use `WithMaximumOf`.
|
||||
type CounterpartAmount: ConvertBack<
|
||||
Perquintill,
|
||||
<Self::Counterpart as FungibleInspect<Self::AccountId>>::Balance,
|
||||
>;
|
||||
|
||||
/// Unbalanced handler to account for funds created (in case of a higher total issuance over
|
||||
/// freezing period).
|
||||
type Deficit: OnUnbalanced<PositiveImbalanceOf<Self>>;
|
||||
|
||||
/// The target sum of all receipts' proportions.
|
||||
type Target: Get<Perquintill>;
|
||||
|
||||
/// Number of duration queues in total. This sets the maximum duration supported, which is
|
||||
/// this value multiplied by `Period`.
|
||||
#[pallet::constant]
|
||||
type QueueCount: Get<u32>;
|
||||
|
||||
/// Maximum number of items that may be in each duration queue.
|
||||
///
|
||||
/// Must be larger than zero.
|
||||
#[pallet::constant]
|
||||
type MaxQueueLen: Get<u32>;
|
||||
|
||||
/// Portion of the queue which is free from ordering and just a FIFO.
|
||||
///
|
||||
/// Must be no greater than `MaxQueueLen`.
|
||||
#[pallet::constant]
|
||||
type FifoQueueLen: Get<u32>;
|
||||
|
||||
/// The base period for the duration queues. This is the common multiple across all
|
||||
/// supported freezing durations that can be bid upon.
|
||||
#[pallet::constant]
|
||||
type BasePeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The minimum amount of funds that may be placed in a bid. Note that this
|
||||
/// does not actually limit the amount which may be represented in a receipt since bids may
|
||||
/// be split up by the system.
|
||||
///
|
||||
/// It should be at least big enough to ensure that there is no possible storage spam attack
|
||||
/// or queue-filling attack.
|
||||
#[pallet::constant]
|
||||
type MinBid: Get<BalanceOf<Self>>;
|
||||
|
||||
/// The minimum amount of funds which may intentionally be left remaining under a single
|
||||
/// receipt.
|
||||
#[pallet::constant]
|
||||
type MinReceipt: Get<Perquintill>;
|
||||
|
||||
/// The number of blocks between consecutive attempts to dequeue bids and create receipts.
|
||||
///
|
||||
/// A larger value results in fewer storage hits each block, but a slower period to get to
|
||||
/// the target.
|
||||
#[pallet::constant]
|
||||
type IntakePeriod: Get<Self::BlockNumber>;
|
||||
|
||||
/// The maximum amount of bids that can consolidated into receipts in a single intake. A
|
||||
/// larger value here means less of the block available for transactions should there be a
|
||||
/// glut of bids.
|
||||
#[pallet::constant]
|
||||
type MaxIntakeWeight: Get<Weight>;
|
||||
|
||||
/// The maximum proportion which may be thawed and the period over which it is reset.
|
||||
#[pallet::constant]
|
||||
type ThawThrottle: Get<(Perquintill, Self::BlockNumber)>;
|
||||
}
|
||||
|
||||
#[pallet::pallet]
|
||||
#[pallet::generate_store(pub(super) trait Store)]
|
||||
pub struct Pallet<T>(_);
|
||||
|
||||
/// A single bid, an item of a *queue* in `Queues`.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct Bid<Balance, AccountId> {
|
||||
/// The amount bid.
|
||||
pub amount: Balance,
|
||||
/// The owner of the bid.
|
||||
pub who: AccountId,
|
||||
}
|
||||
|
||||
/// Information representing a receipt.
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct ReceiptRecord<AccountId, BlockNumber> {
|
||||
/// The proportion of the effective total issuance.
|
||||
pub proportion: Perquintill,
|
||||
/// The account to whom this receipt belongs.
|
||||
pub who: AccountId,
|
||||
/// The time after which this receipt can be thawed.
|
||||
pub expiry: BlockNumber,
|
||||
}
|
||||
|
||||
/// An index for a receipt.
|
||||
pub type ReceiptIndex = u32;
|
||||
|
||||
/// Overall information package on the outstanding receipts.
|
||||
///
|
||||
/// The way of determining the net issuance (i.e. after factoring in all maturing frozen funds)
|
||||
/// is:
|
||||
///
|
||||
/// `issuance - frozen + proportion * issuance`
|
||||
///
|
||||
/// where `issuance = total_issuance - IgnoredIssuance`
|
||||
#[derive(
|
||||
Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
|
||||
)]
|
||||
pub struct SummaryRecord<BlockNumber> {
|
||||
/// The total proportion over all outstanding receipts.
|
||||
pub proportion_owed: Perquintill,
|
||||
/// The total number of receipts created so far.
|
||||
pub index: ReceiptIndex,
|
||||
/// The amount (as a proportion of ETI) which has been thawed in this period so far.
|
||||
pub thawed: Perquintill,
|
||||
/// The current thaw period's beginning.
|
||||
pub last_period: BlockNumber,
|
||||
}
|
||||
|
||||
pub struct OnEmptyQueueTotals<T>(sp_std::marker::PhantomData<T>);
|
||||
impl<T: Config> Get<QueueTotalsTypeOf<T>> for OnEmptyQueueTotals<T> {
|
||||
fn get() -> QueueTotalsTypeOf<T> {
|
||||
BoundedVec::truncate_from(vec![
|
||||
(0, Zero::zero());
|
||||
<T as Config>::QueueCount::get() as usize
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
/// The totals of items and balances within each queue. Saves a lot of storage reads in the
|
||||
/// case of sparsely packed queues.
|
||||
///
|
||||
/// The vector is indexed by duration in `Period`s, offset by one, so information on the queue
|
||||
/// whose duration is one `Period` would be storage `0`.
|
||||
#[pallet::storage]
|
||||
pub type QueueTotals<T: Config> =
|
||||
StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
|
||||
|
||||
/// The queues of bids. Indexed by duration (in `Period`s).
|
||||
#[pallet::storage]
|
||||
pub type Queues<T: Config> =
|
||||
StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
|
||||
|
||||
/// Summary information over the general state.
|
||||
#[pallet::storage]
|
||||
pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
|
||||
|
||||
/// The currently outstanding receipts, indexed according to the order of creation.
|
||||
#[pallet::storage]
|
||||
pub type Receipts<T> =
|
||||
StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf<T>, OptionQuery>;
|
||||
|
||||
#[pallet::event]
|
||||
#[pallet::generate_deposit(pub(super) fn deposit_event)]
|
||||
pub enum Event<T: Config> {
|
||||
/// A bid was successfully placed.
|
||||
BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was successfully removed (before being accepted).
|
||||
BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was dropped from a queue because of another, more substantial, bid was present.
|
||||
BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
|
||||
/// A bid was accepted. The balance may not be released until expiry.
|
||||
Issued {
|
||||
/// The identity of the receipt.
|
||||
index: ReceiptIndex,
|
||||
/// The block number at which the receipt may be thawed.
|
||||
expiry: T::BlockNumber,
|
||||
/// The owner of the receipt.
|
||||
who: T::AccountId,
|
||||
/// The proportion of the effective total issuance which the receipt represents.
|
||||
proportion: Perquintill,
|
||||
/// The amount of funds which were debited from the owner.
|
||||
amount: BalanceOf<T>,
|
||||
},
|
||||
/// An receipt has been (at least partially) thawed.
|
||||
Thawed {
|
||||
/// The identity of the receipt.
|
||||
index: ReceiptIndex,
|
||||
/// The owner.
|
||||
who: T::AccountId,
|
||||
/// The proportion of the effective total issuance by which the owner was debited.
|
||||
proportion: Perquintill,
|
||||
/// The amount by which the owner was credited.
|
||||
amount: BalanceOf<T>,
|
||||
/// If `true` then the receipt is done.
|
||||
dropped: bool,
|
||||
},
|
||||
/// An automatic funding of the deficit was made.
|
||||
Funded { deficit: BalanceOf<T> },
|
||||
/// A receipt was transfered.
|
||||
Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
|
||||
}
|
||||
|
||||
#[pallet::error]
|
||||
pub enum Error<T> {
|
||||
/// The duration of the bid is less than one.
|
||||
DurationTooSmall,
|
||||
/// The duration is the bid is greater than the number of queues.
|
||||
DurationTooBig,
|
||||
/// The amount of the bid is less than the minimum allowed.
|
||||
AmountTooSmall,
|
||||
/// The queue for the bid's duration is full and the amount bid is too low to get in
|
||||
/// through replacing an existing bid.
|
||||
BidTooLow,
|
||||
/// Bond index is unknown.
|
||||
Unknown,
|
||||
/// Not the owner of the receipt.
|
||||
NotOwner,
|
||||
/// Bond not yet at expiry date.
|
||||
NotExpired,
|
||||
/// The given bid for retraction is not found.
|
||||
NotFound,
|
||||
/// The portion supplied is beyond the value of the receipt.
|
||||
TooMuch,
|
||||
/// Not enough funds are held to pay out.
|
||||
Unfunded,
|
||||
/// There are enough funds for what is required.
|
||||
Funded,
|
||||
/// The thaw throttle has been reached for this period.
|
||||
Throttled,
|
||||
/// The operation would result in a receipt worth an insignficant value.
|
||||
MakesDust,
|
||||
}
|
||||
|
||||
pub(crate) struct WeightCounter {
|
||||
pub(crate) used: Weight,
|
||||
pub(crate) limit: Weight,
|
||||
}
|
||||
impl WeightCounter {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn unlimited() -> Self {
|
||||
WeightCounter { used: Weight::zero(), limit: Weight::max_value() }
|
||||
}
|
||||
fn check_accrue(&mut self, w: Weight) -> bool {
|
||||
let test = self.used.saturating_add(w);
|
||||
if test.any_gt(self.limit) {
|
||||
false
|
||||
} else {
|
||||
self.used = test;
|
||||
true
|
||||
}
|
||||
}
|
||||
fn can_accrue(&mut self, w: Weight) -> bool {
|
||||
self.used.saturating_add(w).all_lte(self.limit)
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_initialize(n: T::BlockNumber) -> Weight {
|
||||
let mut weight_counter =
|
||||
WeightCounter { used: Weight::zero(), limit: T::MaxIntakeWeight::get() };
|
||||
if T::IntakePeriod::get().is_zero() || (n % T::IntakePeriod::get()).is_zero() {
|
||||
if weight_counter.check_accrue(T::WeightInfo::process_queues()) {
|
||||
Self::process_queues(
|
||||
T::Target::get(),
|
||||
T::QueueCount::get(),
|
||||
u32::max_value(),
|
||||
&mut weight_counter,
|
||||
)
|
||||
}
|
||||
}
|
||||
weight_counter.used
|
||||
}
|
||||
|
||||
fn integrity_test() {
|
||||
assert!(!T::IntakePeriod::get().is_zero());
|
||||
assert!(!T::MaxQueueLen::get().is_zero());
|
||||
}
|
||||
}
|
||||
|
||||
#[pallet::call]
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// Place a bid.
|
||||
///
|
||||
/// Origin must be Signed, and account must have at least `amount` in free balance.
|
||||
///
|
||||
/// - `amount`: The amount of the bid; these funds will be reserved, and if/when
|
||||
/// consolidated, removed. Must be at least `MinBid`.
|
||||
/// - `duration`: The number of periods before which the newly consolidated bid may be
|
||||
/// thawed. Must be greater than 1 and no more than `QueueCount`.
|
||||
///
|
||||
/// Complexities:
|
||||
/// - `Queues[duration].len()` (just take max).
|
||||
#[pallet::weight(T::WeightInfo::place_bid_max())]
|
||||
pub fn place_bid(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
duration: u32,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
ensure!(amount >= T::MinBid::get(), Error::<T>::AmountTooSmall);
|
||||
let queue_count = T::QueueCount::get() as usize;
|
||||
let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
|
||||
ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
|
||||
|
||||
let net = Queues::<T>::try_mutate(
|
||||
duration,
|
||||
|q| -> Result<(u32, BalanceOf<T>), DispatchError> {
|
||||
let queue_full = q.len() == T::MaxQueueLen::get() as usize;
|
||||
ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
|
||||
T::Currency::reserve(&who, amount)?;
|
||||
|
||||
// queue is <Ordered: Lowest ... Highest><Fifo: Last ... First>
|
||||
let mut bid = Bid { amount, who: who.clone() };
|
||||
let net = if queue_full {
|
||||
sp_std::mem::swap(&mut q[0], &mut bid);
|
||||
let _ = T::Currency::unreserve(&bid.who, bid.amount);
|
||||
Self::deposit_event(Event::<T>::BidDropped {
|
||||
who: bid.who,
|
||||
amount: bid.amount,
|
||||
duration,
|
||||
});
|
||||
(0, amount - bid.amount)
|
||||
} else {
|
||||
q.try_insert(0, bid).expect("verified queue was not full above. qed.");
|
||||
(1, amount)
|
||||
};
|
||||
|
||||
let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
|
||||
if sorted_item_count > 1 {
|
||||
q[0..sorted_item_count].sort_by_key(|x| x.amount);
|
||||
}
|
||||
|
||||
Ok(net)
|
||||
},
|
||||
)?;
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
qs.bounded_resize(queue_count, (0, Zero::zero()));
|
||||
qs[queue_index].0 += net.0;
|
||||
qs[queue_index].1.saturating_accrue(net.1);
|
||||
});
|
||||
Self::deposit_event(Event::BidPlaced { who, amount, duration });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Retract a previously placed bid.
|
||||
///
|
||||
/// Origin must be Signed, and the account should have previously issued a still-active bid
|
||||
/// of `amount` for `duration`.
|
||||
///
|
||||
/// - `amount`: The amount of the previous bid.
|
||||
/// - `duration`: The duration of the previous bid.
|
||||
#[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))]
|
||||
pub fn retract_bid(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] amount: BalanceOf<T>,
|
||||
duration: u32,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
let queue_count = T::QueueCount::get() as usize;
|
||||
let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
|
||||
ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
|
||||
|
||||
let bid = Bid { amount, who };
|
||||
let new_len = Queues::<T>::try_mutate(duration, |q| -> Result<u32, DispatchError> {
|
||||
let pos = q.iter().position(|i| i == &bid).ok_or(Error::<T>::NotFound)?;
|
||||
q.remove(pos);
|
||||
Ok(q.len() as u32)
|
||||
})?;
|
||||
|
||||
QueueTotals::<T>::mutate(|qs| {
|
||||
qs.bounded_resize(queue_count, (0, Zero::zero()));
|
||||
qs[queue_index].0 = new_len;
|
||||
qs[queue_index].1.saturating_reduce(bid.amount);
|
||||
});
|
||||
|
||||
T::Currency::unreserve(&bid.who, bid.amount);
|
||||
Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure we have sufficient funding for all potential payouts.
|
||||
///
|
||||
/// - `origin`: Must be accepted by `FundOrigin`.
|
||||
#[pallet::weight(T::WeightInfo::fund_deficit())]
|
||||
pub fn fund_deficit(origin: OriginFor<T>) -> DispatchResult {
|
||||
T::FundOrigin::ensure_origin(origin)?;
|
||||
let summary: SummaryRecordOf<T> = Summary::<T>::get();
|
||||
let our_account = Self::account_id();
|
||||
let issuance = Self::issuance_with(&our_account, &summary);
|
||||
let deficit = issuance.required.saturating_sub(issuance.holdings);
|
||||
ensure!(!deficit.is_zero(), Error::<T>::Funded);
|
||||
T::Deficit::on_unbalanced(T::Currency::deposit_creating(&our_account, deficit));
|
||||
Self::deposit_event(Event::<T>::Funded { deficit });
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reduce or remove an outstanding receipt, placing the according proportion of funds into
|
||||
/// the account of the owner.
|
||||
///
|
||||
/// - `origin`: Must be Signed and the account must be the owner of the receipt `index` as
|
||||
/// well as any fungible counterpart.
|
||||
/// - `index`: The index of the receipt.
|
||||
/// - `portion`: If `Some`, then only the given portion of the receipt should be thawed. If
|
||||
/// `None`, then all of it should be.
|
||||
#[pallet::weight(T::WeightInfo::thaw())]
|
||||
pub fn thaw(
|
||||
origin: OriginFor<T>,
|
||||
#[pallet::compact] index: ReceiptIndex,
|
||||
portion: Option<<T::Counterpart as FungibleInspect<T::AccountId>>::Balance>,
|
||||
) -> DispatchResult {
|
||||
let who = ensure_signed(origin)?;
|
||||
|
||||
// Look for `index`
|
||||
let mut receipt: ReceiptRecordOf<T> =
|
||||
Receipts::<T>::get(index).ok_or(Error::<T>::Unknown)?;
|
||||
// If found, check the owner is `who`.
|
||||
ensure!(receipt.who == who, Error::<T>::NotOwner);
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
|
||||
|
||||
let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
|
||||
|
||||
let proportion = if let Some(counterpart) = portion {
|
||||
let proportion = T::CounterpartAmount::convert_back(counterpart);
|
||||
ensure!(proportion <= receipt.proportion, Error::<T>::TooMuch);
|
||||
let remaining = receipt.proportion.saturating_sub(proportion);
|
||||
ensure!(
|
||||
remaining.is_zero() || remaining >= T::MinReceipt::get(),
|
||||
Error::<T>::MakesDust
|
||||
);
|
||||
proportion
|
||||
} else {
|
||||
receipt.proportion
|
||||
};
|
||||
|
||||
let (throttle, throttle_period) = T::ThawThrottle::get();
|
||||
if now.saturating_sub(summary.last_period) >= throttle_period {
|
||||
summary.thawed = Zero::zero();
|
||||
summary.last_period = now;
|
||||
}
|
||||
summary.thawed.saturating_accrue(proportion);
|
||||
ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
|
||||
|
||||
T::Counterpart::burn_from(&who, T::CounterpartAmount::convert(proportion))?;
|
||||
|
||||
// Multiply the proportion it is by the total issued.
|
||||
let our_account = Self::account_id();
|
||||
let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
|
||||
let amount = proportion * effective_issuance;
|
||||
|
||||
receipt.proportion.saturating_reduce(proportion);
|
||||
summary.proportion_owed.saturating_reduce(proportion);
|
||||
|
||||
T::Currency::transfer(&our_account, &who, amount, AllowDeath)
|
||||
.map_err(|_| Error::<T>::Unfunded)?;
|
||||
|
||||
let dropped = receipt.proportion.is_zero();
|
||||
if dropped {
|
||||
Receipts::<T>::remove(index);
|
||||
} else {
|
||||
Receipts::<T>::insert(index, &receipt);
|
||||
}
|
||||
Summary::<T>::put(&summary);
|
||||
|
||||
Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Issuance information returned by `issuance()`.
|
||||
#[derive(RuntimeDebug)]
|
||||
pub struct IssuanceInfo<Balance> {
|
||||
/// The balance held in reserve by this pallet instance.
|
||||
pub holdings: Balance,
|
||||
/// The (non-ignored) issuance in the system, not including this pallet's account.
|
||||
pub other: Balance,
|
||||
/// The effective total issuance, hypothetically if all outstanding receipts were thawed at
|
||||
/// present.
|
||||
pub effective: Balance,
|
||||
/// The amount needed to be the pallet instance's account in case all outstanding receipts
|
||||
/// were thawed at present.
|
||||
pub required: Balance,
|
||||
}
|
||||
|
||||
impl<T: Config> NonfungibleInspect<T::AccountId> for Pallet<T> {
|
||||
type ItemId = ReceiptIndex;
|
||||
|
||||
fn owner(item: &ReceiptIndex) -> Option<T::AccountId> {
|
||||
Receipts::<T>::get(item).map(|r| r.who)
|
||||
}
|
||||
|
||||
fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
|
||||
let item = Receipts::<T>::get(item)?;
|
||||
match key {
|
||||
b"proportion" => Some(item.proportion.encode()),
|
||||
b"expiry" => Some(item.expiry.encode()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> NonfungibleTransfer<T::AccountId> for Pallet<T> {
|
||||
fn transfer(index: &ReceiptIndex, destination: &T::AccountId) -> DispatchResult {
|
||||
let mut item = Receipts::<T>::get(index).ok_or(TokenError::UnknownAsset)?;
|
||||
let from = item.who;
|
||||
item.who = destination.clone();
|
||||
Receipts::<T>::insert(&index, &item);
|
||||
Pallet::<T>::deposit_event(Event::<T>::Transferred {
|
||||
from,
|
||||
to: item.who,
|
||||
index: *index,
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Pallet<T> {
|
||||
/// The account ID of the reserves.
|
||||
///
|
||||
/// This actually does computation. If you need to keep using it, then make sure you cache
|
||||
/// the value and only call this once.
|
||||
pub fn account_id() -> T::AccountId {
|
||||
T::PalletId::get().into_account_truncating()
|
||||
}
|
||||
|
||||
/// Returns information on the issuance within the system.
|
||||
pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
|
||||
Self::issuance_with(&Self::account_id(), &Summary::<T>::get())
|
||||
}
|
||||
|
||||
/// Returns information on the issuance within the system
|
||||
///
|
||||
/// This function is equivalent to `issuance`, except that it accepts arguments rather than
|
||||
/// queries state. If the arguments are already known, then this may be slightly more
|
||||
/// performant.
|
||||
///
|
||||
/// - `our_account`: The value returned by `Self::account_id()`.
|
||||
/// - `summary`: The value returned by `Summary::<T>::get()`.
|
||||
pub fn issuance_with(
|
||||
our_account: &T::AccountId,
|
||||
summary: &SummaryRecordOf<T>,
|
||||
) -> IssuanceInfo<BalanceOf<T>> {
|
||||
let total_issuance =
|
||||
T::Currency::total_issuance().saturating_sub(T::IgnoredIssuance::get());
|
||||
let holdings = T::Currency::free_balance(our_account);
|
||||
let other = total_issuance.saturating_sub(holdings);
|
||||
let effective =
|
||||
summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other);
|
||||
let required = summary.proportion_owed * effective;
|
||||
IssuanceInfo { holdings, other, effective, required }
|
||||
}
|
||||
|
||||
/// Process some bids into receipts up to a `target` total of all receipts.
|
||||
///
|
||||
/// Touch at most `max_queues`.
|
||||
///
|
||||
/// Return the weight used.
|
||||
pub(crate) fn process_queues(
|
||||
target: Perquintill,
|
||||
max_queues: u32,
|
||||
max_bids: u32,
|
||||
weight: &mut WeightCounter,
|
||||
) {
|
||||
let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
|
||||
if summary.proportion_owed >= target {
|
||||
return
|
||||
}
|
||||
|
||||
let now = frame_system::Pallet::<T>::block_number();
|
||||
let our_account = Self::account_id();
|
||||
let issuance: IssuanceInfoOf<T> = Self::issuance_with(&our_account, &summary);
|
||||
let mut remaining = target.saturating_sub(summary.proportion_owed) * issuance.effective;
|
||||
|
||||
let mut queues_hit = 0;
|
||||
let mut bids_hit = 0;
|
||||
let mut totals = QueueTotals::<T>::get();
|
||||
let queue_count = T::QueueCount::get();
|
||||
totals.bounded_resize(queue_count as usize, (0, Zero::zero()));
|
||||
for duration in (1..=queue_count).rev() {
|
||||
if totals[duration as usize - 1].0.is_zero() {
|
||||
continue
|
||||
}
|
||||
if remaining.is_zero() || queues_hit >= max_queues
|
||||
|| !weight.check_accrue(T::WeightInfo::process_queue())
|
||||
// No point trying to process a queue if we can't process a single bid.
|
||||
|| !weight.can_accrue(T::WeightInfo::process_bid())
|
||||
{
|
||||
break
|
||||
}
|
||||
|
||||
let b = Self::process_queue(
|
||||
duration,
|
||||
now,
|
||||
&our_account,
|
||||
&issuance,
|
||||
max_bids.saturating_sub(bids_hit),
|
||||
&mut remaining,
|
||||
&mut totals[duration as usize - 1],
|
||||
&mut summary,
|
||||
weight,
|
||||
);
|
||||
|
||||
bids_hit.saturating_accrue(b);
|
||||
queues_hit.saturating_inc();
|
||||
}
|
||||
QueueTotals::<T>::put(&totals);
|
||||
Summary::<T>::put(&summary);
|
||||
}
|
||||
|
||||
pub(crate) fn process_queue(
|
||||
duration: u32,
|
||||
now: T::BlockNumber,
|
||||
our_account: &T::AccountId,
|
||||
issuance: &IssuanceInfo<BalanceOf<T>>,
|
||||
max_bids: u32,
|
||||
remaining: &mut BalanceOf<T>,
|
||||
queue_total: &mut (u32, BalanceOf<T>),
|
||||
summary: &mut SummaryRecordOf<T>,
|
||||
weight: &mut WeightCounter,
|
||||
) -> u32 {
|
||||
let mut queue: BoundedVec<BidOf<T>, _> = Queues::<T>::get(&duration);
|
||||
let expiry = now.saturating_add(T::BasePeriod::get().saturating_mul(duration.into()));
|
||||
let mut count = 0;
|
||||
|
||||
while count < max_bids &&
|
||||
!queue.is_empty() &&
|
||||
!remaining.is_zero() &&
|
||||
weight.check_accrue(T::WeightInfo::process_bid())
|
||||
{
|
||||
let bid = match queue.pop() {
|
||||
Some(b) => b,
|
||||
None => break,
|
||||
};
|
||||
if let Some(bid) = Self::process_bid(
|
||||
bid,
|
||||
expiry,
|
||||
our_account,
|
||||
issuance,
|
||||
remaining,
|
||||
&mut queue_total.1,
|
||||
summary,
|
||||
) {
|
||||
queue.try_push(bid).expect("just popped, so there must be space. qed");
|
||||
// This should exit at the next iteration (though nothing will break if it
|
||||
// doesn't).
|
||||
}
|
||||
count.saturating_inc();
|
||||
}
|
||||
queue_total.0 = queue.len() as u32;
|
||||
Queues::<T>::insert(&duration, &queue);
|
||||
count
|
||||
}
|
||||
|
||||
pub(crate) fn process_bid(
|
||||
mut bid: BidOf<T>,
|
||||
expiry: T::BlockNumber,
|
||||
our_account: &T::AccountId,
|
||||
issuance: &IssuanceInfo<BalanceOf<T>>,
|
||||
remaining: &mut BalanceOf<T>,
|
||||
queue_amount: &mut BalanceOf<T>,
|
||||
summary: &mut SummaryRecordOf<T>,
|
||||
) -> Option<BidOf<T>> {
|
||||
let result = if *remaining < bid.amount {
|
||||
let overflow = bid.amount - *remaining;
|
||||
bid.amount = *remaining;
|
||||
Some(Bid { amount: overflow, who: bid.who.clone() })
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let amount = bid.amount.saturating_sub(T::Currency::unreserve(&bid.who, bid.amount));
|
||||
if T::Currency::transfer(&bid.who, &our_account, amount, AllowDeath).is_err() {
|
||||
return result
|
||||
}
|
||||
|
||||
// Can never overflow due to block above.
|
||||
remaining.saturating_reduce(amount);
|
||||
// Should never underflow since it should track the total of the
|
||||
// bids exactly, but we'll be defensive.
|
||||
queue_amount.defensive_saturating_reduce(amount);
|
||||
|
||||
// Now to activate the bid...
|
||||
let n = amount;
|
||||
let d = issuance.effective;
|
||||
let proportion = Perquintill::from_rational(n, d);
|
||||
let who = bid.who;
|
||||
let index = summary.index;
|
||||
summary.proportion_owed.defensive_saturating_accrue(proportion);
|
||||
summary.index += 1;
|
||||
|
||||
let e = Event::Issued { index, expiry, who: who.clone(), amount, proportion };
|
||||
Self::deposit_event(e);
|
||||
let receipt = ReceiptRecord { proportion, who: who.clone(), expiry };
|
||||
Receipts::<T>::insert(index, receipt);
|
||||
|
||||
// issue the fungible counterpart
|
||||
let fung_eq = T::CounterpartAmount::convert(proportion);
|
||||
let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user