referrerpolicy=no-referrer-when-downgrade

pallet_nis/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Non-Interactive Staking (NIS) Pallet
19//! A pallet allowing accounts to auction for being frozen and receive open-ended
20//! inflation-protection in return.
21//!
22//! ## Overview
23//!
24//! Lock up tokens, for at least as long as you offer, and be free from both inflation and
25//! intermediate reward or exchange until the tokens become unlocked.
26//!
27//! ## Design
28//!
29//! Queues for each of `1..QueueCount` periods, given in blocks (`Period`). Queues are limited in
30//! size to something sensible, `MaxQueueLen`. A secondary storage item with `QueueCount` x `u32`
31//! elements with the number of items in each queue.
32//!
33//! Queues are split into two parts. The first part is a priority queue based on bid size. The
34//! second part is just a FIFO (the size of the second part is set with `FifoQueueLen`). Items are
35//! always prepended so that removal is always O(1) since removal often happens many times under a
36//! single weighed function (`on_initialize`) yet placing bids only ever happens once per weighed
37//! function (`place_bid`). If the queue has a priority portion, then it remains sorted in order of
38//! bid size so that smaller bids fall off as it gets too large.
39//!
40//! Account may enqueue a balance with some number of `Period`s lock up, up to a maximum of
41//! `QueueCount`. The balance gets reserved. There's a minimum of `MinBid` to avoid dust.
42//!
43//! Until your bid is consolidated and you receive a receipt, you can retract it instantly and the
44//! funds are unreserved.
45//!
46//! There's a target proportion of effective total issuance (i.e. accounting for existing receipts)
47//! which the pallet attempts to have frozen at any one time. It will likely be gradually increased
48//! over time by governance.
49//!
50//! As the proportion of effective total issuance represented by outstanding receipts drops below
51//! `FrozenFraction`, then bids are taken from queues and consolidated into receipts, with the queue
52//! of the greatest period taking priority. If the item in the queue's locked amount is greater than
53//! the amount remaining to achieve `FrozenFraction`, then it is split up into multiple bids and
54//! becomes partially consolidated.
55//!
56//! With the consolidation of a bid, the bid amount is taken from the owner and a receipt is issued.
57//! The receipt records the proportion of the bid compared to effective total issuance at the time
58//! of consolidation. The receipt has two independent elements: a "main" non-fungible receipt and
59//! a second set of fungible "counterpart" tokens. The accounting functionality of the latter must
60//! be provided through the `Counterpart` trait item. The main non-fungible receipt may have its
61//! owner transferred through the pallet's implementation of `nonfungible::Transfer`.
62//!
63//! A later `thaw` function may be called in order to reduce the recorded proportion or entirely
64//! remove the receipt in return for the appropriate proportion of the effective total issuance.
65//! This may happen no earlier than queue's period after the point at which the receipt was issued.
66//! The call must be made by the owner of both the "main" non-fungible receipt and the appropriate
67//! amount of counterpart tokens.
68//!
69//! `NoCounterpart` may be provided as an implementation for the counterpart token system in which
70//! case they are completely disregarded from the thawing logic.
71//!
72//! ## Terms
73//!
74//! - *Effective total issuance*: The total issuance of balances in the system, equal to the active
75//!   issuance plus the value of all outstanding receipts, less `IgnoredIssuance`.
76
77#![cfg_attr(not(feature = "std"), no_std)]
78
79extern crate alloc;
80
81pub mod weights;
82
83mod benchmarking;
84#[cfg(test)]
85mod mock;
86#[cfg(test)]
87mod tests;
88
89pub use pallet::*;
90pub use weights::WeightInfo;
91
92use alloc::{vec, vec::Vec};
93use frame::prelude::*;
94use fungible::{
95	Balanced as FunBalanced, Inspect as FunInspect, Mutate as FunMutate,
96	MutateHold as FunMutateHold,
97};
98use nonfungible::{Inspect as NftInspect, Transfer as NftTransfer};
99use tokens::{Balance, Restriction::*};
100use Fortitude::*;
101use Precision::*;
102use Preservation::*;
103
104pub struct WithMaximumOf<A: TypedGet>(core::marker::PhantomData<A>);
105impl<A: TypedGet> Convert<Perquintill, A::Type> for WithMaximumOf<A>
106where
107	A::Type: Clone + Unsigned + From<u64>,
108	u64: TryFrom<A::Type>,
109{
110	fn convert(a: Perquintill) -> A::Type {
111		a * A::get()
112	}
113}
114impl<A: TypedGet> ConvertBack<Perquintill, A::Type> for WithMaximumOf<A>
115where
116	A::Type: RationalArg + From<u64>,
117	u64: TryFrom<A::Type>,
118	u128: TryFrom<A::Type>,
119{
120	fn convert_back(a: A::Type) -> Perquintill {
121		Perquintill::from_rational(a, A::get())
122	}
123}
124
125pub struct NoCounterpart<T>(core::marker::PhantomData<T>);
126impl<T> FunInspect<T> for NoCounterpart<T> {
127	type Balance = u32;
128	fn total_issuance() -> u32 {
129		0
130	}
131	fn minimum_balance() -> u32 {
132		0
133	}
134	fn balance(_: &T) -> u32 {
135		0
136	}
137	fn total_balance(_: &T) -> u32 {
138		0
139	}
140	fn reducible_balance(_: &T, _: Preservation, _: Fortitude) -> u32 {
141		0
142	}
143	fn can_deposit(_: &T, _: u32, _: Provenance) -> DepositConsequence {
144		DepositConsequence::Success
145	}
146	fn can_withdraw(_: &T, _: u32) -> WithdrawConsequence<u32> {
147		WithdrawConsequence::Success
148	}
149}
150impl<T> fungible::Unbalanced<T> for NoCounterpart<T> {
151	fn handle_dust(_: fungible::Dust<T, Self>) {}
152	fn write_balance(_: &T, _: Self::Balance) -> Result<Option<Self::Balance>, DispatchError> {
153		Ok(None)
154	}
155	fn set_total_issuance(_: Self::Balance) {}
156}
157impl<T: Eq> FunMutate<T> for NoCounterpart<T> {}
158impl<T> Convert<Perquintill, u32> for NoCounterpart<T> {
159	fn convert(_: Perquintill) -> u32 {
160		0
161	}
162}
163
164/// Setup the empty genesis state for benchmarking.
165pub trait BenchmarkSetup {
166	/// Create the counterpart asset. Should panic on error.
167	///
168	/// This is called prior to assuming that a counterpart balance exists.
169	fn create_counterpart_asset();
170}
171
172impl BenchmarkSetup for () {
173	fn create_counterpart_asset() {}
174}
175
176#[frame::pallet]
177pub mod pallet {
178	use super::*;
179
180	type BalanceOf<T> =
181		<<T as Config>::Currency as FunInspect<<T as frame_system::Config>::AccountId>>::Balance;
182	type DebtOf<T> =
183		fungible::Debt<<T as frame_system::Config>::AccountId, <T as Config>::Currency>;
184	type ReceiptRecordOf<T> =
185		ReceiptRecord<<T as frame_system::Config>::AccountId, BlockNumberFor<T>, BalanceOf<T>>;
186	type IssuanceInfoOf<T> = IssuanceInfo<BalanceOf<T>>;
187	type SummaryRecordOf<T> = SummaryRecord<BlockNumberFor<T>, BalanceOf<T>>;
188	type BidOf<T> = Bid<BalanceOf<T>, <T as frame_system::Config>::AccountId>;
189	type QueueTotalsTypeOf<T> = BoundedVec<(u32, BalanceOf<T>), <T as Config>::QueueCount>;
190
191	#[pallet::config]
192	pub trait Config: frame_system::Config {
193		/// Information on runtime weights.
194		type WeightInfo: WeightInfo;
195
196		/// Overarching event type.
197		#[allow(deprecated)]
198		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
199
200		/// The treasury's pallet id, used for deriving its sovereign account ID.
201		#[pallet::constant]
202		type PalletId: Get<PalletId>;
203
204		/// Currency type that this works on.
205		type Currency: FunInspect<Self::AccountId, Balance = Self::CurrencyBalance>
206			+ FunMutate<Self::AccountId>
207			+ FunBalanced<Self::AccountId>
208			+ FunMutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>;
209
210		/// Overarching hold reason.
211		type RuntimeHoldReason: From<HoldReason>;
212
213		/// Just the [`Balance`] type; we have this item to allow us to constrain it to
214		/// [`From<u64>`].
215		type CurrencyBalance: Balance + From<u64>;
216
217		/// Origin required for auto-funding the deficit.
218		type FundOrigin: EnsureOrigin<Self::RuntimeOrigin>;
219
220		/// The issuance to ignore. This is subtracted from the `Currency`'s `total_issuance` to get
221		/// the issuance with which we determine the thawed value of a given proportion.
222		type IgnoredIssuance: Get<BalanceOf<Self>>;
223
224		/// The accounting system for the fungible counterpart tokens.
225		type Counterpart: FunMutate<Self::AccountId>;
226
227		/// The system to convert an overall proportion of issuance into a number of fungible
228		/// counterpart tokens.
229		///
230		/// In general it's best to use `WithMaximumOf`.
231		type CounterpartAmount: ConvertBack<
232			Perquintill,
233			<Self::Counterpart as FunInspect<Self::AccountId>>::Balance,
234		>;
235
236		/// Unbalanced handler to account for funds created (in case of a higher total issuance over
237		/// freezing period).
238		type Deficit: OnUnbalanced<DebtOf<Self>>;
239
240		/// The target sum of all receipts' proportions.
241		type Target: Get<Perquintill>;
242
243		/// Number of duration queues in total. This sets the maximum duration supported, which is
244		/// this value multiplied by `Period`.
245		#[pallet::constant]
246		type QueueCount: Get<u32>;
247
248		/// Maximum number of items that may be in each duration queue.
249		///
250		/// Must be larger than zero.
251		#[pallet::constant]
252		type MaxQueueLen: Get<u32>;
253
254		/// Portion of the queue which is free from ordering and just a FIFO.
255		///
256		/// Must be no greater than `MaxQueueLen`.
257		#[pallet::constant]
258		type FifoQueueLen: Get<u32>;
259
260		/// The base period for the duration queues. This is the common multiple across all
261		/// supported freezing durations that can be bid upon.
262		#[pallet::constant]
263		type BasePeriod: Get<BlockNumberFor<Self>>;
264
265		/// The minimum amount of funds that may be placed in a bid. Note that this
266		/// does not actually limit the amount which may be represented in a receipt since bids may
267		/// be split up by the system.
268		///
269		/// It should be at least big enough to ensure that there is no possible storage spam attack
270		/// or queue-filling attack.
271		#[pallet::constant]
272		type MinBid: Get<BalanceOf<Self>>;
273
274		/// The minimum amount of funds which may intentionally be left remaining under a single
275		/// receipt.
276		#[pallet::constant]
277		type MinReceipt: Get<Perquintill>;
278
279		/// The number of blocks between consecutive attempts to dequeue bids and create receipts.
280		///
281		/// A larger value results in fewer storage hits each block, but a slower period to get to
282		/// the target.
283		#[pallet::constant]
284		type IntakePeriod: Get<BlockNumberFor<Self>>;
285
286		/// The maximum amount of bids that can consolidated into receipts in a single intake. A
287		/// larger value here means less of the block available for transactions should there be a
288		/// glut of bids.
289		#[pallet::constant]
290		type MaxIntakeWeight: Get<Weight>;
291
292		/// The maximum proportion which may be thawed and the period over which it is reset.
293		#[pallet::constant]
294		type ThawThrottle: Get<(Perquintill, BlockNumberFor<Self>)>;
295
296		/// Setup the state for benchmarking.
297		#[cfg(feature = "runtime-benchmarks")]
298		type BenchmarkSetup: crate::BenchmarkSetup;
299	}
300
301	#[pallet::pallet]
302	pub struct Pallet<T>(_);
303
304	/// A single bid, an item of a *queue* in `Queues`.
305	#[derive(Clone, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
306	pub struct Bid<Balance, AccountId> {
307		/// The amount bid.
308		pub amount: Balance,
309		/// The owner of the bid.
310		pub who: AccountId,
311	}
312
313	/// Information representing a receipt.
314	#[derive(Clone, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
315	pub struct ReceiptRecord<AccountId, BlockNumber, Balance> {
316		/// The proportion of the effective total issuance.
317		pub proportion: Perquintill,
318		/// The account to whom this receipt belongs and the amount of funds on hold in their
319		/// account for servicing this receipt. If `None`, then it is a communal receipt and
320		/// fungible counterparts have been issued.
321		pub owner: Option<(AccountId, Balance)>,
322		/// The time after which this receipt can be thawed.
323		pub expiry: BlockNumber,
324	}
325
326	/// An index for a receipt.
327	pub type ReceiptIndex = u32;
328
329	/// Overall information package on the outstanding receipts.
330	///
331	/// The way of determining the net issuance (i.e. after factoring in all maturing frozen funds)
332	/// is:
333	///
334	/// `issuance - frozen + proportion * issuance`
335	///
336	/// where `issuance = active_issuance - IgnoredIssuance`
337	#[derive(Clone, Eq, PartialEq, Default, Encode, Decode, Debug, TypeInfo, MaxEncodedLen)]
338	pub struct SummaryRecord<BlockNumber, Balance> {
339		/// The total proportion over all outstanding receipts.
340		pub proportion_owed: Perquintill,
341		/// The total number of receipts created so far.
342		pub index: ReceiptIndex,
343		/// The amount (as a proportion of ETI) which has been thawed in this period so far.
344		pub thawed: Perquintill,
345		/// The current thaw period's beginning.
346		pub last_period: BlockNumber,
347		/// The total amount of funds on hold for receipts. This doesn't include the pot or funds
348		/// on hold for bids.
349		pub receipts_on_hold: Balance,
350	}
351
352	pub struct OnEmptyQueueTotals<T>(core::marker::PhantomData<T>);
353	impl<T: Config> Get<QueueTotalsTypeOf<T>> for OnEmptyQueueTotals<T> {
354		fn get() -> QueueTotalsTypeOf<T> {
355			BoundedVec::truncate_from(vec![
356				(0, Zero::zero());
357				<T as Config>::QueueCount::get() as usize
358			])
359		}
360	}
361
362	/// The totals of items and balances within each queue. Saves a lot of storage reads in the
363	/// case of sparsely packed queues.
364	///
365	/// The vector is indexed by duration in `Period`s, offset by one, so information on the queue
366	/// whose duration is one `Period` would be storage `0`.
367	#[pallet::storage]
368	pub type QueueTotals<T: Config> =
369		StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
370
371	/// The queues of bids. Indexed by duration (in `Period`s).
372	#[pallet::storage]
373	pub type Queues<T: Config> =
374		StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
375
376	/// Summary information over the general state.
377	#[pallet::storage]
378	pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
379
380	/// The currently outstanding receipts, indexed according to the order of creation.
381	#[pallet::storage]
382	pub type Receipts<T> =
383		StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf<T>, OptionQuery>;
384
385	#[pallet::event]
386	#[pallet::generate_deposit(pub(super) fn deposit_event)]
387	pub enum Event<T: Config> {
388		/// A bid was successfully placed.
389		BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
390		/// A bid was successfully removed (before being accepted).
391		BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
392		/// A bid was dropped from a queue because of another, more substantial, bid was present.
393		BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
394		/// A bid was accepted. The balance may not be released until expiry.
395		Issued {
396			/// The identity of the receipt.
397			index: ReceiptIndex,
398			/// The block number at which the receipt may be thawed.
399			expiry: BlockNumberFor<T>,
400			/// The owner of the receipt.
401			who: T::AccountId,
402			/// The proportion of the effective total issuance which the receipt represents.
403			proportion: Perquintill,
404			/// The amount of funds which were debited from the owner.
405			amount: BalanceOf<T>,
406		},
407		/// An receipt has been (at least partially) thawed.
408		Thawed {
409			/// The identity of the receipt.
410			index: ReceiptIndex,
411			/// The owner.
412			who: T::AccountId,
413			/// The proportion of the effective total issuance by which the owner was debited.
414			proportion: Perquintill,
415			/// The amount by which the owner was credited.
416			amount: BalanceOf<T>,
417			/// If `true` then the receipt is done.
418			dropped: bool,
419		},
420		/// An automatic funding of the deficit was made.
421		Funded { deficit: BalanceOf<T> },
422		/// A receipt was transferred.
423		Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
424	}
425
426	#[pallet::error]
427	pub enum Error<T> {
428		/// The duration of the bid is less than one.
429		DurationTooSmall,
430		/// The duration is the bid is greater than the number of queues.
431		DurationTooBig,
432		/// The amount of the bid is less than the minimum allowed.
433		AmountTooSmall,
434		/// The queue for the bid's duration is full and the amount bid is too low to get in
435		/// through replacing an existing bid.
436		BidTooLow,
437		/// Receipt index is unknown.
438		UnknownReceipt,
439		/// Not the owner of the receipt.
440		NotOwner,
441		/// Bond not yet at expiry date.
442		NotExpired,
443		/// The given bid for retraction is not found.
444		UnknownBid,
445		/// The portion supplied is beyond the value of the receipt.
446		PortionTooBig,
447		/// Not enough funds are held to pay out.
448		Unfunded,
449		/// There are enough funds for what is required.
450		AlreadyFunded,
451		/// The thaw throttle has been reached for this period.
452		Throttled,
453		/// The operation would result in a receipt worth an insignificant value.
454		MakesDust,
455		/// The receipt is already communal.
456		AlreadyCommunal,
457		/// The receipt is already private.
458		AlreadyPrivate,
459	}
460
461	/// A reason for the NIS pallet placing a hold on funds.
462	#[pallet::composite_enum]
463	pub enum HoldReason {
464		/// The NIS Pallet has reserved it for a non-fungible receipt.
465		#[codec(index = 0)]
466		NftReceipt,
467	}
468
469	pub(crate) struct WeightCounter {
470		pub(crate) used: Weight,
471		pub(crate) limit: Weight,
472	}
473	impl WeightCounter {
474		#[allow(dead_code)]
475		pub(crate) fn unlimited() -> Self {
476			WeightCounter { used: Weight::zero(), limit: Weight::max_value() }
477		}
478		fn check_accrue(&mut self, w: Weight) -> bool {
479			let test = self.used.saturating_add(w);
480			if test.any_gt(self.limit) {
481				false
482			} else {
483				self.used = test;
484				true
485			}
486		}
487		fn can_accrue(&mut self, w: Weight) -> bool {
488			self.used.saturating_add(w).all_lte(self.limit)
489		}
490	}
491
492	#[pallet::hooks]
493	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
494		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
495			let mut weight_counter =
496				WeightCounter { used: Weight::zero(), limit: T::MaxIntakeWeight::get() };
497			if T::IntakePeriod::get().is_zero() || (n % T::IntakePeriod::get()).is_zero() {
498				if weight_counter.check_accrue(T::WeightInfo::process_queues()) {
499					Self::process_queues(
500						T::Target::get(),
501						T::QueueCount::get(),
502						u32::max_value(),
503						&mut weight_counter,
504					)
505				}
506			}
507			weight_counter.used
508		}
509
510		fn integrity_test() {
511			assert!(!T::IntakePeriod::get().is_zero());
512			assert!(!T::MaxQueueLen::get().is_zero());
513		}
514	}
515
516	#[pallet::call]
517	impl<T: Config> Pallet<T> {
518		/// Place a bid.
519		///
520		/// Origin must be Signed, and account must have at least `amount` in free balance.
521		///
522		/// - `amount`: The amount of the bid; these funds will be reserved, and if/when
523		///   consolidated, removed. Must be at least `MinBid`.
524		/// - `duration`: The number of periods before which the newly consolidated bid may be
525		///   thawed. Must be greater than 1 and no more than `QueueCount`.
526		///
527		/// Complexities:
528		/// - `Queues[duration].len()` (just take max).
529		#[pallet::call_index(0)]
530		#[pallet::weight(T::WeightInfo::place_bid_max())]
531		pub fn place_bid(
532			origin: OriginFor<T>,
533			#[pallet::compact] amount: BalanceOf<T>,
534			duration: u32,
535		) -> DispatchResult {
536			let who = ensure_signed(origin)?;
537
538			ensure!(amount >= T::MinBid::get(), Error::<T>::AmountTooSmall);
539			let queue_count = T::QueueCount::get() as usize;
540			let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
541			ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
542
543			let net = Queues::<T>::try_mutate(
544				duration,
545				|q| -> Result<(u32, BalanceOf<T>), DispatchError> {
546					let queue_full = q.len() == T::MaxQueueLen::get() as usize;
547					ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
548					T::Currency::hold(&HoldReason::NftReceipt.into(), &who, amount)?;
549
550					// queue is <Ordered: Lowest ... Highest><Fifo: Last ... First>
551					let mut bid = Bid { amount, who: who.clone() };
552					let net = if queue_full {
553						core::mem::swap(&mut q[0], &mut bid);
554						let _ = T::Currency::release(
555							&HoldReason::NftReceipt.into(),
556							&bid.who,
557							bid.amount,
558							BestEffort,
559						);
560						Self::deposit_event(Event::<T>::BidDropped {
561							who: bid.who,
562							amount: bid.amount,
563							duration,
564						});
565						(0, amount - bid.amount)
566					} else {
567						q.try_insert(0, bid).expect("verified queue was not full above. qed.");
568						(1, amount)
569					};
570
571					let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
572					if sorted_item_count > 1 {
573						q[0..sorted_item_count].sort_by_key(|x| x.amount);
574					}
575
576					Ok(net)
577				},
578			)?;
579			QueueTotals::<T>::mutate(|qs| {
580				qs.bounded_resize(queue_count, (0, Zero::zero()));
581				qs[queue_index].0 += net.0;
582				qs[queue_index].1.saturating_accrue(net.1);
583			});
584			Self::deposit_event(Event::BidPlaced { who, amount, duration });
585
586			Ok(())
587		}
588
589		/// Retract a previously placed bid.
590		///
591		/// Origin must be Signed, and the account should have previously issued a still-active bid
592		/// of `amount` for `duration`.
593		///
594		/// - `amount`: The amount of the previous bid.
595		/// - `duration`: The duration of the previous bid.
596		#[pallet::call_index(1)]
597		#[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))]
598		pub fn retract_bid(
599			origin: OriginFor<T>,
600			#[pallet::compact] amount: BalanceOf<T>,
601			duration: u32,
602		) -> DispatchResult {
603			let who = ensure_signed(origin)?;
604
605			let queue_count = T::QueueCount::get() as usize;
606			let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
607			ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
608
609			let bid = Bid { amount, who };
610
611			let mut queue = Queues::<T>::get(duration);
612			let pos = queue.iter().position(|i| i == &bid).ok_or(Error::<T>::UnknownBid)?;
613			queue.remove(pos);
614			let new_len = queue.len() as u32;
615
616			T::Currency::release(&HoldReason::NftReceipt.into(), &bid.who, bid.amount, BestEffort)?;
617
618			Queues::<T>::insert(duration, queue);
619			QueueTotals::<T>::mutate(|qs| {
620				qs.bounded_resize(queue_count, (0, Zero::zero()));
621				qs[queue_index].0 = new_len;
622				qs[queue_index].1.saturating_reduce(bid.amount);
623			});
624
625			Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
626
627			Ok(())
628		}
629
630		/// Ensure we have sufficient funding for all potential payouts.
631		///
632		/// - `origin`: Must be accepted by `FundOrigin`.
633		#[pallet::call_index(2)]
634		#[pallet::weight(T::WeightInfo::fund_deficit())]
635		pub fn fund_deficit(origin: OriginFor<T>) -> DispatchResult {
636			T::FundOrigin::ensure_origin(origin)?;
637			let summary: SummaryRecordOf<T> = Summary::<T>::get();
638			let our_account = Self::account_id();
639			let issuance = Self::issuance_with(&our_account, &summary);
640			let deficit = issuance.required.saturating_sub(issuance.holdings);
641			ensure!(!deficit.is_zero(), Error::<T>::AlreadyFunded);
642			T::Deficit::on_unbalanced(T::Currency::deposit(&our_account, deficit, Exact)?);
643			Self::deposit_event(Event::<T>::Funded { deficit });
644			Ok(())
645		}
646
647		/// Reduce or remove an outstanding receipt, placing the according proportion of funds into
648		/// the account of the owner.
649		///
650		/// - `origin`: Must be Signed and the account must be the owner of the receipt `index` as
651		///   well as any fungible counterpart.
652		/// - `index`: The index of the receipt.
653		/// - `portion`: If `Some`, then only the given portion of the receipt should be thawed. If
654		///   `None`, then all of it should be.
655		#[pallet::call_index(3)]
656		#[pallet::weight(T::WeightInfo::thaw_private())]
657		pub fn thaw_private(
658			origin: OriginFor<T>,
659			#[pallet::compact] index: ReceiptIndex,
660			maybe_proportion: Option<Perquintill>,
661		) -> DispatchResult {
662			let who = ensure_signed(origin)?;
663
664			// Look for `index`
665			let mut receipt: ReceiptRecordOf<T> =
666				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
667			// If found, check the owner is `who`.
668			let (owner, mut on_hold) = receipt.owner.ok_or(Error::<T>::AlreadyCommunal)?;
669			ensure!(owner == who, Error::<T>::NotOwner);
670
671			let now = frame_system::Pallet::<T>::block_number();
672			ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
673
674			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
675
676			let proportion = if let Some(proportion) = maybe_proportion {
677				ensure!(proportion <= receipt.proportion, Error::<T>::PortionTooBig);
678				let remaining = receipt.proportion.saturating_sub(proportion);
679				ensure!(
680					remaining.is_zero() || remaining >= T::MinReceipt::get(),
681					Error::<T>::MakesDust
682				);
683				proportion
684			} else {
685				receipt.proportion
686			};
687
688			let (throttle, throttle_period) = T::ThawThrottle::get();
689			if now.saturating_sub(summary.last_period) >= throttle_period {
690				summary.thawed = Zero::zero();
691				summary.last_period = now;
692			}
693			summary.thawed.saturating_accrue(proportion);
694			ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
695
696			// Multiply the proportion it is by the total issued.
697			let our_account = Self::account_id();
698			let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
699			//			let amount = proportion.mul_ceil(effective_issuance);
700			let amount = proportion * effective_issuance;
701
702			receipt.proportion.saturating_reduce(proportion);
703			summary.proportion_owed.saturating_reduce(proportion);
704
705			let dropped = receipt.proportion.is_zero();
706
707			if amount > on_hold {
708				T::Currency::release(&HoldReason::NftReceipt.into(), &who, on_hold, Exact)?;
709				let deficit = amount - on_hold;
710				// Try to transfer deficit from pot to receipt owner.
711				summary.receipts_on_hold.saturating_reduce(on_hold);
712				on_hold = Zero::zero();
713				T::Currency::transfer(&our_account, &who, deficit, Expendable)
714					.map_err(|_| Error::<T>::Unfunded)?;
715			} else {
716				on_hold.saturating_reduce(amount);
717				summary.receipts_on_hold.saturating_reduce(amount);
718				if dropped && !on_hold.is_zero() {
719					// Reclaim any remainder:
720					// Transfer excess of `on_hold` to the pot if we have now fully compensated for
721					// the receipt.
722					T::Currency::transfer_on_hold(
723						&HoldReason::NftReceipt.into(),
724						&who,
725						&our_account,
726						on_hold,
727						Exact,
728						Free,
729						Polite,
730					)
731					.map(|_| ())
732					// We ignore this error as it just means the amount we're trying to deposit is
733					// dust and the beneficiary account doesn't exist.
734					.or_else(|e| {
735						if e == TokenError::CannotCreate.into() {
736							Ok(())
737						} else {
738							Err(e)
739						}
740					})?;
741					summary.receipts_on_hold.saturating_reduce(on_hold);
742				}
743				T::Currency::release(&HoldReason::NftReceipt.into(), &who, amount, Exact)?;
744			}
745
746			if dropped {
747				Receipts::<T>::remove(index);
748			} else {
749				receipt.owner = Some((owner, on_hold));
750				Receipts::<T>::insert(index, &receipt);
751			}
752			Summary::<T>::put(&summary);
753
754			Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped });
755
756			Ok(())
757		}
758
759		/// Reduce or remove an outstanding receipt, placing the according proportion of funds into
760		/// the account of the owner.
761		///
762		/// - `origin`: Must be Signed and the account must be the owner of the fungible counterpart
763		///   for receipt `index`.
764		/// - `index`: The index of the receipt.
765		#[pallet::call_index(4)]
766		#[pallet::weight(T::WeightInfo::thaw_communal())]
767		pub fn thaw_communal(
768			origin: OriginFor<T>,
769			#[pallet::compact] index: ReceiptIndex,
770		) -> DispatchResult {
771			let who = ensure_signed(origin)?;
772
773			// Look for `index`
774			let receipt: ReceiptRecordOf<T> =
775				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
776			// If found, check it is actually communal.
777			ensure!(receipt.owner.is_none(), Error::<T>::NotOwner);
778			let now = frame_system::Pallet::<T>::block_number();
779			ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
780
781			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
782
783			let (throttle, throttle_period) = T::ThawThrottle::get();
784			if now.saturating_sub(summary.last_period) >= throttle_period {
785				summary.thawed = Zero::zero();
786				summary.last_period = now;
787			}
788			summary.thawed.saturating_accrue(receipt.proportion);
789			ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
790
791			let cp_amount = T::CounterpartAmount::convert(receipt.proportion);
792			T::Counterpart::burn_from(&who, cp_amount, Expendable, Exact, Polite)?;
793
794			// Multiply the proportion it is by the total issued.
795			let our_account = Self::account_id();
796			let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
797			let amount = receipt.proportion * effective_issuance;
798
799			summary.proportion_owed.saturating_reduce(receipt.proportion);
800
801			// Try to transfer amount owed from pot to receipt owner.
802			T::Currency::transfer(&our_account, &who, amount, Expendable)
803				.map_err(|_| Error::<T>::Unfunded)?;
804
805			Receipts::<T>::remove(index);
806			Summary::<T>::put(&summary);
807
808			let e =
809				Event::Thawed { index, who, amount, proportion: receipt.proportion, dropped: true };
810			Self::deposit_event(e);
811
812			Ok(())
813		}
814
815		/// Make a private receipt communal and create fungible counterparts for its owner.
816		#[pallet::call_index(5)]
817		#[pallet::weight(T::WeightInfo::communify())]
818		pub fn communify(
819			origin: OriginFor<T>,
820			#[pallet::compact] index: ReceiptIndex,
821		) -> DispatchResult {
822			let who = ensure_signed(origin)?;
823
824			// Look for `index`
825			let mut receipt: ReceiptRecordOf<T> =
826				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
827
828			// Check it's not already communal and make it so.
829			let (owner, on_hold) = receipt.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
830
831			// If found, check the owner is `who`.
832			ensure!(owner == who, Error::<T>::NotOwner);
833
834			// Unreserve and transfer the funds to the pot.
835			let reason = HoldReason::NftReceipt.into();
836			let us = Self::account_id();
837			T::Currency::transfer_on_hold(&reason, &who, &us, on_hold, Exact, Free, Polite)
838				.map_err(|_| Error::<T>::Unfunded)?;
839
840			// Record that we've moved the amount reserved.
841			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
842			summary.receipts_on_hold.saturating_reduce(on_hold);
843			Summary::<T>::put(&summary);
844			Receipts::<T>::insert(index, &receipt);
845
846			// Mint fungibles.
847			let fung_eq = T::CounterpartAmount::convert(receipt.proportion);
848			let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
849
850			Ok(())
851		}
852
853		/// Make a communal receipt private and burn fungible counterparts from its owner.
854		#[pallet::call_index(6)]
855		#[pallet::weight(T::WeightInfo::privatize())]
856		pub fn privatize(
857			origin: OriginFor<T>,
858			#[pallet::compact] index: ReceiptIndex,
859		) -> DispatchResult {
860			let who = ensure_signed(origin)?;
861
862			// Look for `index`
863			let mut receipt: ReceiptRecordOf<T> =
864				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
865
866			// If found, check there is no owner.
867			ensure!(receipt.owner.is_none(), Error::<T>::AlreadyPrivate);
868
869			// Multiply the proportion it is by the total issued.
870			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
871			let our_account = Self::account_id();
872			let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
873			let max_amount = receipt.proportion * effective_issuance;
874			// Avoid trying to place more in the account's reserve than we have available in the pot
875			let amount = max_amount.min(T::Currency::balance(&our_account));
876
877			// Burn fungible counterparts.
878			T::Counterpart::burn_from(
879				&who,
880				T::CounterpartAmount::convert(receipt.proportion),
881				Expendable,
882				Exact,
883				Polite,
884			)?;
885
886			// Transfer the funds from the pot to the owner and reserve
887			let reason = HoldReason::NftReceipt.into();
888			let us = Self::account_id();
889			T::Currency::transfer_and_hold(&reason, &us, &who, amount, Exact, Expendable, Polite)?;
890
891			// Record that we've moved the amount reserved.
892			summary.receipts_on_hold.saturating_accrue(amount);
893
894			receipt.owner = Some((who, amount));
895
896			Summary::<T>::put(&summary);
897			Receipts::<T>::insert(index, &receipt);
898
899			Ok(())
900		}
901	}
902
903	/// Issuance information returned by `issuance()`.
904	#[derive(Debug)]
905	pub struct IssuanceInfo<Balance> {
906		/// The balance held by this pallet instance together with the balances on hold across
907		/// all receipt-owning accounts.
908		pub holdings: Balance,
909		/// The (non-ignored) issuance in the system, not including this pallet's account.
910		pub other: Balance,
911		/// The effective total issuance, hypothetically if all outstanding receipts were thawed at
912		/// present.
913		pub effective: Balance,
914		/// The amount needed to be accessible to this pallet in case all outstanding receipts were
915		/// thawed at present. If it is more than `holdings`, then the pallet will need funding.
916		pub required: Balance,
917	}
918
919	impl<T: Config> NftInspect<T::AccountId> for Pallet<T> {
920		type ItemId = ReceiptIndex;
921
922		fn owner(item: &ReceiptIndex) -> Option<T::AccountId> {
923			Receipts::<T>::get(item).and_then(|r| r.owner).map(|(who, _)| who)
924		}
925
926		fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
927			let item = Receipts::<T>::get(item)?;
928			match key {
929				b"proportion" => Some(item.proportion.encode()),
930				b"expiry" => Some(item.expiry.encode()),
931				b"owner" => item.owner.as_ref().map(|x| x.0.encode()),
932				b"on_hold" => item.owner.as_ref().map(|x| x.1.encode()),
933				_ => None,
934			}
935		}
936	}
937
938	impl<T: Config> NftTransfer<T::AccountId> for Pallet<T> {
939		fn transfer(index: &ReceiptIndex, dest: &T::AccountId) -> DispatchResult {
940			let mut item = Receipts::<T>::get(index).ok_or(TokenError::UnknownAsset)?;
941			let (owner, on_hold) = item.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
942
943			let reason = HoldReason::NftReceipt.into();
944			T::Currency::transfer_on_hold(&reason, &owner, dest, on_hold, Exact, OnHold, Polite)?;
945
946			item.owner = Some((dest.clone(), on_hold));
947			Receipts::<T>::insert(&index, &item);
948			Pallet::<T>::deposit_event(Event::<T>::Transferred {
949				from: owner,
950				to: dest.clone(),
951				index: *index,
952			});
953			Ok(())
954		}
955	}
956
957	impl<T: Config> Pallet<T> {
958		/// The account ID of the reserves.
959		///
960		/// This actually does computation. If you need to keep using it, then make sure you cache
961		/// the value and only call this once.
962		pub fn account_id() -> T::AccountId {
963			T::PalletId::get().into_account_truncating()
964		}
965
966		/// Returns information on the issuance within the system.
967		pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
968			Self::issuance_with(&Self::account_id(), &Summary::<T>::get())
969		}
970
971		/// Returns information on the issuance within the system
972		///
973		/// This function is equivalent to `issuance`, except that it accepts arguments rather than
974		/// queries state. If the arguments are already known, then this may be slightly more
975		/// performant.
976		///
977		/// - `our_account`: The value returned by `Self::account_id()`.
978		/// - `summary`: The value returned by `Summary::<T>::get()`.
979		pub fn issuance_with(
980			our_account: &T::AccountId,
981			summary: &SummaryRecordOf<T>,
982		) -> IssuanceInfo<BalanceOf<T>> {
983			let total_issuance =
984				T::Currency::active_issuance().saturating_sub(T::IgnoredIssuance::get());
985			let holdings =
986				T::Currency::balance(our_account).saturating_add(summary.receipts_on_hold);
987			let other = total_issuance.saturating_sub(holdings);
988			let effective =
989				summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other);
990			let required = summary.proportion_owed * effective;
991			IssuanceInfo { holdings, other, effective, required }
992		}
993
994		/// Process some bids into receipts up to a `target` total of all receipts.
995		///
996		/// Touch at most `max_queues`.
997		///
998		/// Return the weight used.
999		pub(crate) fn process_queues(
1000			target: Perquintill,
1001			max_queues: u32,
1002			max_bids: u32,
1003			weight: &mut WeightCounter,
1004		) {
1005			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
1006			if summary.proportion_owed >= target {
1007				return
1008			}
1009
1010			let now = frame_system::Pallet::<T>::block_number();
1011			let our_account = Self::account_id();
1012			let issuance: IssuanceInfoOf<T> = Self::issuance_with(&our_account, &summary);
1013			let mut remaining = target.saturating_sub(summary.proportion_owed) * issuance.effective;
1014
1015			let mut queues_hit = 0;
1016			let mut bids_hit = 0;
1017			let mut totals = QueueTotals::<T>::get();
1018			let queue_count = T::QueueCount::get();
1019			totals.bounded_resize(queue_count as usize, (0, Zero::zero()));
1020			for duration in (1..=queue_count).rev() {
1021				if totals[duration as usize - 1].0.is_zero() {
1022					continue
1023				}
1024				if remaining.is_zero() || queues_hit >= max_queues
1025					|| !weight.check_accrue(T::WeightInfo::process_queue())
1026					// No point trying to process a queue if we can't process a single bid.
1027					|| !weight.can_accrue(T::WeightInfo::process_bid())
1028				{
1029					break
1030				}
1031
1032				let b = Self::process_queue(
1033					duration,
1034					now,
1035					&our_account,
1036					&issuance,
1037					max_bids.saturating_sub(bids_hit),
1038					&mut remaining,
1039					&mut totals[duration as usize - 1],
1040					&mut summary,
1041					weight,
1042				);
1043
1044				bids_hit.saturating_accrue(b);
1045				queues_hit.saturating_inc();
1046			}
1047			QueueTotals::<T>::put(&totals);
1048			Summary::<T>::put(&summary);
1049		}
1050
1051		pub(crate) fn process_queue(
1052			duration: u32,
1053			now: BlockNumberFor<T>,
1054			our_account: &T::AccountId,
1055			issuance: &IssuanceInfo<BalanceOf<T>>,
1056			max_bids: u32,
1057			remaining: &mut BalanceOf<T>,
1058			queue_total: &mut (u32, BalanceOf<T>),
1059			summary: &mut SummaryRecordOf<T>,
1060			weight: &mut WeightCounter,
1061		) -> u32 {
1062			let mut queue: BoundedVec<BidOf<T>, _> = Queues::<T>::get(&duration);
1063			let expiry = now.saturating_add(T::BasePeriod::get().saturating_mul(duration.into()));
1064			let mut count = 0;
1065
1066			while count < max_bids &&
1067				!queue.is_empty() &&
1068				!remaining.is_zero() &&
1069				weight.check_accrue(T::WeightInfo::process_bid())
1070			{
1071				let bid = match queue.pop() {
1072					Some(b) => b,
1073					None => break,
1074				};
1075				if let Some(bid) = Self::process_bid(
1076					bid,
1077					expiry,
1078					our_account,
1079					issuance,
1080					remaining,
1081					&mut queue_total.1,
1082					summary,
1083				) {
1084					queue.try_push(bid).expect("just popped, so there must be space. qed");
1085					// This should exit at the next iteration (though nothing will break if it
1086					// doesn't).
1087				}
1088				count.saturating_inc();
1089			}
1090			queue_total.0 = queue.len() as u32;
1091			Queues::<T>::insert(&duration, &queue);
1092			count
1093		}
1094
1095		pub(crate) fn process_bid(
1096			mut bid: BidOf<T>,
1097			expiry: BlockNumberFor<T>,
1098			_our_account: &T::AccountId,
1099			issuance: &IssuanceInfo<BalanceOf<T>>,
1100			remaining: &mut BalanceOf<T>,
1101			queue_amount: &mut BalanceOf<T>,
1102			summary: &mut SummaryRecordOf<T>,
1103		) -> Option<BidOf<T>> {
1104			let result = if *remaining < bid.amount {
1105				let overflow = bid.amount - *remaining;
1106				bid.amount = *remaining;
1107				Some(Bid { amount: overflow, who: bid.who.clone() })
1108			} else {
1109				None
1110			};
1111			let amount = bid.amount;
1112			summary.receipts_on_hold.saturating_accrue(amount);
1113
1114			// Can never overflow due to block above.
1115			remaining.saturating_reduce(amount);
1116			// Should never underflow since it should track the total of the
1117			// bids exactly, but we'll be defensive.
1118			queue_amount.defensive_saturating_reduce(amount);
1119
1120			// Now to activate the bid...
1121			let n = amount;
1122			let d = issuance.effective;
1123			let proportion = Perquintill::from_rational_with_rounding(n, d, Rounding::Down)
1124				.defensive_unwrap_or_default();
1125			let who = bid.who;
1126			let index = summary.index;
1127			summary.proportion_owed.defensive_saturating_accrue(proportion);
1128			summary.index += 1;
1129
1130			let e = Event::Issued { index, expiry, who: who.clone(), amount, proportion };
1131			Self::deposit_event(e);
1132			let receipt = ReceiptRecord { proportion, owner: Some((who, amount)), expiry };
1133			Receipts::<T>::insert(index, receipt);
1134
1135			result
1136		}
1137	}
1138}