// This file is part of Substrate. // 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. //! # 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, equal to the active //! issuance plus the value of all outstanding receipts, less `IgnoredIssuance`. #![cfg_attr(not(feature = "std"), no_std)] use frame_support::traits::{ fungible::{self, Inspect as FunInspect, Mutate as FunMutate}, tokens::{DepositConsequence, Fortitude, Preservation, Provenance, WithdrawConsequence}, }; pub use pallet::*; use sp_arithmetic::{traits::Unsigned, RationalArg}; use sp_core::TypedGet; use sp_runtime::{ traits::{Convert, ConvertBack}, DispatchError, Perquintill, }; mod benchmarking; #[cfg(test)] mod mock; #[cfg(test)] mod tests; pub mod weights; pub struct WithMaximumOf(sp_std::marker::PhantomData); impl Convert for WithMaximumOf where A::Type: Clone + Unsigned + From, u64: TryFrom, { fn convert(a: Perquintill) -> A::Type { a * A::get() } } impl ConvertBack for WithMaximumOf where A::Type: RationalArg + From, u64: TryFrom, u128: TryFrom, { fn convert_back(a: A::Type) -> Perquintill { Perquintill::from_rational(a, A::get()) } } pub struct NoCounterpart(sp_std::marker::PhantomData); impl FunInspect for NoCounterpart { type Balance = u32; fn total_issuance() -> u32 { 0 } fn minimum_balance() -> u32 { 0 } fn balance(_: &T) -> u32 { 0 } fn total_balance(_: &T) -> u32 { 0 } fn reducible_balance(_: &T, _: Preservation, _: Fortitude) -> u32 { 0 } fn can_deposit(_: &T, _: u32, _: Provenance) -> DepositConsequence { DepositConsequence::Success } fn can_withdraw(_: &T, _: u32) -> WithdrawConsequence { WithdrawConsequence::Success } } impl fungible::Unbalanced for NoCounterpart { fn handle_dust(_: fungible::Dust) {} fn write_balance(_: &T, _: Self::Balance) -> Result, DispatchError> { Ok(None) } fn set_total_issuance(_: Self::Balance) {} } impl FunMutate for NoCounterpart {} impl Convert for NoCounterpart { fn convert(_: Perquintill) -> u32 { 0 } } #[frame_support::pallet] pub mod pallet { use super::{FunInspect, FunMutate}; pub use crate::weights::WeightInfo; use frame_support::{ pallet_prelude::*, traits::{ fungible::{self, hold::Mutate as FunHoldMutate, Balanced as FunBalanced}, nonfungible::{Inspect as NftInspect, Transfer as NftTransfer}, tokens::{ Balance, Fortitude::Polite, Precision::{BestEffort, Exact}, Preservation::Expendable, Restriction::{Free, OnHold}, }, Defensive, DefensiveSaturating, OnUnbalanced, }, PalletId, }; use frame_system::pallet_prelude::*; use sp_arithmetic::{PerThing, Perquintill}; use sp_runtime::{ traits::{AccountIdConversion, Bounded, Convert, ConvertBack, Saturating, Zero}, Rounding, TokenError, }; use sp_std::prelude::*; type BalanceOf = <::Currency as FunInspect<::AccountId>>::Balance; type DebtOf = fungible::Debt<::AccountId, ::Currency>; type ReceiptRecordOf = ReceiptRecord<::AccountId, BlockNumberFor, BalanceOf>; type IssuanceInfoOf = IssuanceInfo>; type SummaryRecordOf = SummaryRecord, BalanceOf>; type BidOf = Bid, ::AccountId>; type QueueTotalsTypeOf = BoundedVec<(u32, BalanceOf), ::QueueCount>; #[pallet::config] pub trait Config: frame_system::Config { /// Information on runtime weights. type WeightInfo: WeightInfo; /// Overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The treasury's pallet id, used for deriving its sovereign account ID. #[pallet::constant] type PalletId: Get; /// Currency type that this works on. type Currency: FunInspect + FunMutate + FunBalanced + FunHoldMutate; /// Overarching hold reason. type RuntimeHoldReason: From; /// Just the [`Balance`] type; we have this item to allow us to constrain it to /// [`From`]. type CurrencyBalance: Balance + From; /// Origin required for auto-funding the deficit. type FundOrigin: EnsureOrigin; /// 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>; /// The accounting system for the fungible counterpart tokens. type Counterpart: FunMutate; /// 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, >::Balance, >; /// Unbalanced handler to account for funds created (in case of a higher total issuance over /// freezing period). type Deficit: OnUnbalanced>; /// The target sum of all receipts' proportions. type Target: Get; /// Number of duration queues in total. This sets the maximum duration supported, which is /// this value multiplied by `Period`. #[pallet::constant] type QueueCount: Get; /// Maximum number of items that may be in each duration queue. /// /// Must be larger than zero. #[pallet::constant] type MaxQueueLen: Get; /// Portion of the queue which is free from ordering and just a FIFO. /// /// Must be no greater than `MaxQueueLen`. #[pallet::constant] type FifoQueueLen: Get; /// 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>; /// 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>; /// The minimum amount of funds which may intentionally be left remaining under a single /// receipt. #[pallet::constant] type MinReceipt: Get; /// 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>; /// 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; /// The maximum proportion which may be thawed and the period over which it is reset. #[pallet::constant] type ThawThrottle: Get<(Perquintill, BlockNumberFor)>; } #[pallet::pallet] pub struct Pallet(_); /// A single bid, an item of a *queue* in `Queues`. #[derive( Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, )] pub struct Bid { /// 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 { /// The proportion of the effective total issuance. pub proportion: Perquintill, /// The account to whom this receipt belongs and the amount of funds on hold in their /// account for servicing this receipt. If `None`, then it is a communal receipt and /// fungible counterparts have been issued. pub owner: Option<(AccountId, Balance)>, /// 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 = active_issuance - IgnoredIssuance` #[derive( Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen, )] pub struct SummaryRecord { /// 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, /// The total amount of funds on hold for receipts. This doesn't include the pot or funds /// on hold for bids. pub receipts_on_hold: Balance, } pub struct OnEmptyQueueTotals(sp_std::marker::PhantomData); impl Get> for OnEmptyQueueTotals { fn get() -> QueueTotalsTypeOf { BoundedVec::truncate_from(vec![ (0, Zero::zero()); ::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 = StorageValue<_, QueueTotalsTypeOf, ValueQuery, OnEmptyQueueTotals>; /// The queues of bids. Indexed by duration (in `Period`s). #[pallet::storage] pub type Queues = StorageMap<_, Blake2_128Concat, u32, BoundedVec, T::MaxQueueLen>, ValueQuery>; /// Summary information over the general state. #[pallet::storage] pub type Summary = StorageValue<_, SummaryRecordOf, ValueQuery>; /// The currently outstanding receipts, indexed according to the order of creation. #[pallet::storage] pub type Receipts = StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf, OptionQuery>; #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// A bid was successfully placed. BidPlaced { who: T::AccountId, amount: BalanceOf, duration: u32 }, /// A bid was successfully removed (before being accepted). BidRetracted { who: T::AccountId, amount: BalanceOf, duration: u32 }, /// A bid was dropped from a queue because of another, more substantial, bid was present. BidDropped { who: T::AccountId, amount: BalanceOf, 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: BlockNumberFor, /// 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, }, /// 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, /// If `true` then the receipt is done. dropped: bool, }, /// An automatic funding of the deficit was made. Funded { deficit: BalanceOf }, /// A receipt was transfered. Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex }, } #[pallet::error] pub enum Error { /// 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, /// Receipt index is unknown. UnknownReceipt, /// Not the owner of the receipt. NotOwner, /// Bond not yet at expiry date. NotExpired, /// The given bid for retraction is not found. UnknownBid, /// The portion supplied is beyond the value of the receipt. PortionTooBig, /// Not enough funds are held to pay out. Unfunded, /// There are enough funds for what is required. AlreadyFunded, /// The thaw throttle has been reached for this period. Throttled, /// The operation would result in a receipt worth an insignficant value. MakesDust, /// The receipt is already communal. AlreadyCommunal, /// The receipt is already private. AlreadyPrivate, } /// A reason for the NIS pallet placing a hold on funds. #[pallet::composite_enum] pub enum HoldReason { /// The NIS Pallet has reserved it for a non-fungible receipt. #[codec(index = 0)] NftReceipt, } 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 Hooks> for Pallet { fn on_initialize(n: BlockNumberFor) -> 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 Pallet { /// 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::call_index(0)] #[pallet::weight(T::WeightInfo::place_bid_max())] pub fn place_bid( origin: OriginFor, #[pallet::compact] amount: BalanceOf, duration: u32, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!(amount >= T::MinBid::get(), Error::::AmountTooSmall); let queue_count = T::QueueCount::get() as usize; let queue_index = duration.checked_sub(1).ok_or(Error::::DurationTooSmall)? as usize; ensure!(queue_index < queue_count, Error::::DurationTooBig); let net = Queues::::try_mutate( duration, |q| -> Result<(u32, BalanceOf), DispatchError> { let queue_full = q.len() == T::MaxQueueLen::get() as usize; ensure!(!queue_full || q[0].amount < amount, Error::::BidTooLow); T::Currency::hold(&HoldReason::NftReceipt.into(), &who, amount)?; // queue is 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::release( &HoldReason::NftReceipt.into(), &bid.who, bid.amount, BestEffort, ); Self::deposit_event(Event::::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::::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::call_index(1)] #[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))] pub fn retract_bid( origin: OriginFor, #[pallet::compact] amount: BalanceOf, 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::::DurationTooSmall)? as usize; ensure!(queue_index < queue_count, Error::::DurationTooBig); let bid = Bid { amount, who }; let mut queue = Queues::::get(duration); let pos = queue.iter().position(|i| i == &bid).ok_or(Error::::UnknownBid)?; queue.remove(pos); let new_len = queue.len() as u32; T::Currency::release(&HoldReason::NftReceipt.into(), &bid.who, bid.amount, BestEffort)?; Queues::::insert(duration, queue); QueueTotals::::mutate(|qs| { qs.bounded_resize(queue_count, (0, Zero::zero())); qs[queue_index].0 = new_len; qs[queue_index].1.saturating_reduce(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::call_index(2)] #[pallet::weight(T::WeightInfo::fund_deficit())] pub fn fund_deficit(origin: OriginFor) -> DispatchResult { T::FundOrigin::ensure_origin(origin)?; let summary: SummaryRecordOf = Summary::::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::::AlreadyFunded); T::Deficit::on_unbalanced(T::Currency::deposit(&our_account, deficit, Exact)?); Self::deposit_event(Event::::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::call_index(3)] #[pallet::weight(T::WeightInfo::thaw_private())] pub fn thaw_private( origin: OriginFor, #[pallet::compact] index: ReceiptIndex, maybe_proportion: Option, ) -> DispatchResult { let who = ensure_signed(origin)?; // Look for `index` let mut receipt: ReceiptRecordOf = Receipts::::get(index).ok_or(Error::::UnknownReceipt)?; // If found, check the owner is `who`. let (owner, mut on_hold) = receipt.owner.ok_or(Error::::AlreadyCommunal)?; ensure!(owner == who, Error::::NotOwner); let now = frame_system::Pallet::::block_number(); ensure!(now >= receipt.expiry, Error::::NotExpired); let mut summary: SummaryRecordOf = Summary::::get(); let proportion = if let Some(proportion) = maybe_proportion { ensure!(proportion <= receipt.proportion, Error::::PortionTooBig); let remaining = receipt.proportion.saturating_sub(proportion); ensure!( remaining.is_zero() || remaining >= T::MinReceipt::get(), Error::::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::::Throttled); // 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.mul_ceil(effective_issuance); let amount = proportion * effective_issuance; receipt.proportion.saturating_reduce(proportion); summary.proportion_owed.saturating_reduce(proportion); let dropped = receipt.proportion.is_zero(); if amount > on_hold { T::Currency::release(&HoldReason::NftReceipt.into(), &who, on_hold, Exact)?; let deficit = amount - on_hold; // Try to transfer deficit from pot to receipt owner. summary.receipts_on_hold.saturating_reduce(on_hold); on_hold = Zero::zero(); T::Currency::transfer(&our_account, &who, deficit, Expendable) .map_err(|_| Error::::Unfunded)?; } else { on_hold.saturating_reduce(amount); summary.receipts_on_hold.saturating_reduce(amount); if dropped && !on_hold.is_zero() { // Reclaim any remainder: // Transfer excess of `on_hold` to the pot if we have now fully compensated for // the receipt. T::Currency::transfer_on_hold( &HoldReason::NftReceipt.into(), &who, &our_account, on_hold, Exact, Free, Polite, ) .map(|_| ()) // We ignore this error as it just means the amount we're trying to deposit is // dust and the beneficiary account doesn't exist. .or_else( |e| if e == TokenError::CannotCreate.into() { Ok(()) } else { Err(e) }, )?; summary.receipts_on_hold.saturating_reduce(on_hold); } T::Currency::release(&HoldReason::NftReceipt.into(), &who, amount, Exact)?; } if dropped { Receipts::::remove(index); } else { receipt.owner = Some((owner, on_hold)); Receipts::::insert(index, &receipt); } Summary::::put(&summary); Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped }); 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 fungible counterpart /// for receipt `index`. /// - `index`: The index of the receipt. #[pallet::call_index(4)] #[pallet::weight(T::WeightInfo::thaw_communal())] pub fn thaw_communal( origin: OriginFor, #[pallet::compact] index: ReceiptIndex, ) -> DispatchResult { let who = ensure_signed(origin)?; // Look for `index` let receipt: ReceiptRecordOf = Receipts::::get(index).ok_or(Error::::UnknownReceipt)?; // If found, check it is actually communal. ensure!(receipt.owner.is_none(), Error::::NotOwner); let now = frame_system::Pallet::::block_number(); ensure!(now >= receipt.expiry, Error::::NotExpired); let mut summary: SummaryRecordOf = Summary::::get(); 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(receipt.proportion); ensure!(summary.thawed <= throttle, Error::::Throttled); let cp_amount = T::CounterpartAmount::convert(receipt.proportion); T::Counterpart::burn_from(&who, cp_amount, Exact, Polite)?; // 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 = receipt.proportion * effective_issuance; summary.proportion_owed.saturating_reduce(receipt.proportion); // Try to transfer amount owed from pot to receipt owner. T::Currency::transfer(&our_account, &who, amount, Expendable) .map_err(|_| Error::::Unfunded)?; Receipts::::remove(index); Summary::::put(&summary); let e = Event::Thawed { index, who, amount, proportion: receipt.proportion, dropped: true }; Self::deposit_event(e); Ok(()) } /// Make a private receipt communal and create fungible counterparts for its owner. #[pallet::call_index(5)] #[pallet::weight(T::WeightInfo::communify())] pub fn communify( origin: OriginFor, #[pallet::compact] index: ReceiptIndex, ) -> DispatchResult { let who = ensure_signed(origin)?; // Look for `index` let mut receipt: ReceiptRecordOf = Receipts::::get(index).ok_or(Error::::UnknownReceipt)?; // Check it's not already communal and make it so. let (owner, on_hold) = receipt.owner.take().ok_or(Error::::AlreadyCommunal)?; // If found, check the owner is `who`. ensure!(owner == who, Error::::NotOwner); // Unreserve and transfer the funds to the pot. let reason = HoldReason::NftReceipt.into(); let us = Self::account_id(); T::Currency::transfer_on_hold(&reason, &who, &us, on_hold, Exact, Free, Polite) .map_err(|_| Error::::Unfunded)?; // Record that we've moved the amount reserved. let mut summary: SummaryRecordOf = Summary::::get(); summary.receipts_on_hold.saturating_reduce(on_hold); Summary::::put(&summary); Receipts::::insert(index, &receipt); // Mint fungibles. let fung_eq = T::CounterpartAmount::convert(receipt.proportion); let _ = T::Counterpart::mint_into(&who, fung_eq).defensive(); Ok(()) } /// Make a communal receipt private and burn fungible counterparts from its owner. #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::privatize())] pub fn privatize( origin: OriginFor, #[pallet::compact] index: ReceiptIndex, ) -> DispatchResult { let who = ensure_signed(origin)?; // Look for `index` let mut receipt: ReceiptRecordOf = Receipts::::get(index).ok_or(Error::::UnknownReceipt)?; // If found, check there is no owner. ensure!(receipt.owner.is_none(), Error::::AlreadyPrivate); // Multiply the proportion it is by the total issued. let mut summary: SummaryRecordOf = Summary::::get(); let our_account = Self::account_id(); let effective_issuance = Self::issuance_with(&our_account, &summary).effective; let max_amount = receipt.proportion * effective_issuance; // Avoid trying to place more in the account's reserve than we have available in the pot let amount = max_amount.min(T::Currency::balance(&our_account)); // Burn fungible counterparts. T::Counterpart::burn_from( &who, T::CounterpartAmount::convert(receipt.proportion), Exact, Polite, )?; // Transfer the funds from the pot to the owner and reserve let reason = HoldReason::NftReceipt.into(); let us = Self::account_id(); T::Currency::transfer_and_hold(&reason, &us, &who, amount, Exact, Expendable, Polite)?; // Record that we've moved the amount reserved. summary.receipts_on_hold.saturating_accrue(amount); receipt.owner = Some((who, amount)); Summary::::put(&summary); Receipts::::insert(index, &receipt); Ok(()) } } /// Issuance information returned by `issuance()`. #[derive(Debug)] pub struct IssuanceInfo { /// The balance held by this pallet instance together with the balances on hold across /// all receipt-owning accounts. 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 accessible to this pallet in case all outstanding receipts were /// thawed at present. If it is more than `holdings`, then the pallet will need funding. pub required: Balance, } impl NftInspect for Pallet { type ItemId = ReceiptIndex; fn owner(item: &ReceiptIndex) -> Option { Receipts::::get(item).and_then(|r| r.owner).map(|(who, _)| who) } fn attribute(item: &Self::ItemId, key: &[u8]) -> Option> { let item = Receipts::::get(item)?; match key { b"proportion" => Some(item.proportion.encode()), b"expiry" => Some(item.expiry.encode()), b"owner" => item.owner.as_ref().map(|x| x.0.encode()), b"on_hold" => item.owner.as_ref().map(|x| x.1.encode()), _ => None, } } } impl NftTransfer for Pallet { fn transfer(index: &ReceiptIndex, dest: &T::AccountId) -> DispatchResult { let mut item = Receipts::::get(index).ok_or(TokenError::UnknownAsset)?; let (owner, on_hold) = item.owner.take().ok_or(Error::::AlreadyCommunal)?; let reason = HoldReason::NftReceipt.into(); T::Currency::transfer_on_hold(&reason, &owner, dest, on_hold, Exact, OnHold, Polite)?; item.owner = Some((dest.clone(), on_hold)); Receipts::::insert(&index, &item); Pallet::::deposit_event(Event::::Transferred { from: owner, to: dest.clone(), index: *index, }); Ok(()) } } impl Pallet { /// 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> { Self::issuance_with(&Self::account_id(), &Summary::::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::::get()`. pub fn issuance_with( our_account: &T::AccountId, summary: &SummaryRecordOf, ) -> IssuanceInfo> { let total_issuance = T::Currency::active_issuance().saturating_sub(T::IgnoredIssuance::get()); let holdings = T::Currency::balance(our_account).saturating_add(summary.receipts_on_hold); 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 = Summary::::get(); if summary.proportion_owed >= target { return } let now = frame_system::Pallet::::block_number(); let our_account = Self::account_id(); let issuance: IssuanceInfoOf = 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::::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::::put(&totals); Summary::::put(&summary); } pub(crate) fn process_queue( duration: u32, now: BlockNumberFor, our_account: &T::AccountId, issuance: &IssuanceInfo>, max_bids: u32, remaining: &mut BalanceOf, queue_total: &mut (u32, BalanceOf), summary: &mut SummaryRecordOf, weight: &mut WeightCounter, ) -> u32 { let mut queue: BoundedVec, _> = Queues::::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::::insert(&duration, &queue); count } pub(crate) fn process_bid( mut bid: BidOf, expiry: BlockNumberFor, _our_account: &T::AccountId, issuance: &IssuanceInfo>, remaining: &mut BalanceOf, queue_amount: &mut BalanceOf, summary: &mut SummaryRecordOf, ) -> Option> { 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; summary.receipts_on_hold.saturating_accrue(amount); // 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_with_rounding(n, d, Rounding::Down) .defensive_unwrap_or_default(); 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, owner: Some((who, amount)), expiry }; Receipts::::insert(index, receipt); result } } }