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(
306		Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
307	)]
308	pub struct Bid<Balance, AccountId> {
309		/// The amount bid.
310		pub amount: Balance,
311		/// The owner of the bid.
312		pub who: AccountId,
313	}
314
315	/// Information representing a receipt.
316	#[derive(
317		Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
318	)]
319	pub struct ReceiptRecord<AccountId, BlockNumber, Balance> {
320		/// The proportion of the effective total issuance.
321		pub proportion: Perquintill,
322		/// The account to whom this receipt belongs and the amount of funds on hold in their
323		/// account for servicing this receipt. If `None`, then it is a communal receipt and
324		/// fungible counterparts have been issued.
325		pub owner: Option<(AccountId, Balance)>,
326		/// The time after which this receipt can be thawed.
327		pub expiry: BlockNumber,
328	}
329
330	/// An index for a receipt.
331	pub type ReceiptIndex = u32;
332
333	/// Overall information package on the outstanding receipts.
334	///
335	/// The way of determining the net issuance (i.e. after factoring in all maturing frozen funds)
336	/// is:
337	///
338	/// `issuance - frozen + proportion * issuance`
339	///
340	/// where `issuance = active_issuance - IgnoredIssuance`
341	#[derive(
342		Clone, Eq, PartialEq, Default, Encode, Decode, RuntimeDebug, TypeInfo, MaxEncodedLen,
343	)]
344	pub struct SummaryRecord<BlockNumber, Balance> {
345		/// The total proportion over all outstanding receipts.
346		pub proportion_owed: Perquintill,
347		/// The total number of receipts created so far.
348		pub index: ReceiptIndex,
349		/// The amount (as a proportion of ETI) which has been thawed in this period so far.
350		pub thawed: Perquintill,
351		/// The current thaw period's beginning.
352		pub last_period: BlockNumber,
353		/// The total amount of funds on hold for receipts. This doesn't include the pot or funds
354		/// on hold for bids.
355		pub receipts_on_hold: Balance,
356	}
357
358	pub struct OnEmptyQueueTotals<T>(core::marker::PhantomData<T>);
359	impl<T: Config> Get<QueueTotalsTypeOf<T>> for OnEmptyQueueTotals<T> {
360		fn get() -> QueueTotalsTypeOf<T> {
361			BoundedVec::truncate_from(vec![
362				(0, Zero::zero());
363				<T as Config>::QueueCount::get() as usize
364			])
365		}
366	}
367
368	/// The totals of items and balances within each queue. Saves a lot of storage reads in the
369	/// case of sparsely packed queues.
370	///
371	/// The vector is indexed by duration in `Period`s, offset by one, so information on the queue
372	/// whose duration is one `Period` would be storage `0`.
373	#[pallet::storage]
374	pub type QueueTotals<T: Config> =
375		StorageValue<_, QueueTotalsTypeOf<T>, ValueQuery, OnEmptyQueueTotals<T>>;
376
377	/// The queues of bids. Indexed by duration (in `Period`s).
378	#[pallet::storage]
379	pub type Queues<T: Config> =
380		StorageMap<_, Blake2_128Concat, u32, BoundedVec<BidOf<T>, T::MaxQueueLen>, ValueQuery>;
381
382	/// Summary information over the general state.
383	#[pallet::storage]
384	pub type Summary<T> = StorageValue<_, SummaryRecordOf<T>, ValueQuery>;
385
386	/// The currently outstanding receipts, indexed according to the order of creation.
387	#[pallet::storage]
388	pub type Receipts<T> =
389		StorageMap<_, Blake2_128Concat, ReceiptIndex, ReceiptRecordOf<T>, OptionQuery>;
390
391	#[pallet::event]
392	#[pallet::generate_deposit(pub(super) fn deposit_event)]
393	pub enum Event<T: Config> {
394		/// A bid was successfully placed.
395		BidPlaced { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
396		/// A bid was successfully removed (before being accepted).
397		BidRetracted { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
398		/// A bid was dropped from a queue because of another, more substantial, bid was present.
399		BidDropped { who: T::AccountId, amount: BalanceOf<T>, duration: u32 },
400		/// A bid was accepted. The balance may not be released until expiry.
401		Issued {
402			/// The identity of the receipt.
403			index: ReceiptIndex,
404			/// The block number at which the receipt may be thawed.
405			expiry: BlockNumberFor<T>,
406			/// The owner of the receipt.
407			who: T::AccountId,
408			/// The proportion of the effective total issuance which the receipt represents.
409			proportion: Perquintill,
410			/// The amount of funds which were debited from the owner.
411			amount: BalanceOf<T>,
412		},
413		/// An receipt has been (at least partially) thawed.
414		Thawed {
415			/// The identity of the receipt.
416			index: ReceiptIndex,
417			/// The owner.
418			who: T::AccountId,
419			/// The proportion of the effective total issuance by which the owner was debited.
420			proportion: Perquintill,
421			/// The amount by which the owner was credited.
422			amount: BalanceOf<T>,
423			/// If `true` then the receipt is done.
424			dropped: bool,
425		},
426		/// An automatic funding of the deficit was made.
427		Funded { deficit: BalanceOf<T> },
428		/// A receipt was transferred.
429		Transferred { from: T::AccountId, to: T::AccountId, index: ReceiptIndex },
430	}
431
432	#[pallet::error]
433	pub enum Error<T> {
434		/// The duration of the bid is less than one.
435		DurationTooSmall,
436		/// The duration is the bid is greater than the number of queues.
437		DurationTooBig,
438		/// The amount of the bid is less than the minimum allowed.
439		AmountTooSmall,
440		/// The queue for the bid's duration is full and the amount bid is too low to get in
441		/// through replacing an existing bid.
442		BidTooLow,
443		/// Receipt index is unknown.
444		UnknownReceipt,
445		/// Not the owner of the receipt.
446		NotOwner,
447		/// Bond not yet at expiry date.
448		NotExpired,
449		/// The given bid for retraction is not found.
450		UnknownBid,
451		/// The portion supplied is beyond the value of the receipt.
452		PortionTooBig,
453		/// Not enough funds are held to pay out.
454		Unfunded,
455		/// There are enough funds for what is required.
456		AlreadyFunded,
457		/// The thaw throttle has been reached for this period.
458		Throttled,
459		/// The operation would result in a receipt worth an insignificant value.
460		MakesDust,
461		/// The receipt is already communal.
462		AlreadyCommunal,
463		/// The receipt is already private.
464		AlreadyPrivate,
465	}
466
467	/// A reason for the NIS pallet placing a hold on funds.
468	#[pallet::composite_enum]
469	pub enum HoldReason {
470		/// The NIS Pallet has reserved it for a non-fungible receipt.
471		#[codec(index = 0)]
472		NftReceipt,
473	}
474
475	pub(crate) struct WeightCounter {
476		pub(crate) used: Weight,
477		pub(crate) limit: Weight,
478	}
479	impl WeightCounter {
480		#[allow(dead_code)]
481		pub(crate) fn unlimited() -> Self {
482			WeightCounter { used: Weight::zero(), limit: Weight::max_value() }
483		}
484		fn check_accrue(&mut self, w: Weight) -> bool {
485			let test = self.used.saturating_add(w);
486			if test.any_gt(self.limit) {
487				false
488			} else {
489				self.used = test;
490				true
491			}
492		}
493		fn can_accrue(&mut self, w: Weight) -> bool {
494			self.used.saturating_add(w).all_lte(self.limit)
495		}
496	}
497
498	#[pallet::hooks]
499	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
500		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
501			let mut weight_counter =
502				WeightCounter { used: Weight::zero(), limit: T::MaxIntakeWeight::get() };
503			if T::IntakePeriod::get().is_zero() || (n % T::IntakePeriod::get()).is_zero() {
504				if weight_counter.check_accrue(T::WeightInfo::process_queues()) {
505					Self::process_queues(
506						T::Target::get(),
507						T::QueueCount::get(),
508						u32::max_value(),
509						&mut weight_counter,
510					)
511				}
512			}
513			weight_counter.used
514		}
515
516		fn integrity_test() {
517			assert!(!T::IntakePeriod::get().is_zero());
518			assert!(!T::MaxQueueLen::get().is_zero());
519		}
520	}
521
522	#[pallet::call]
523	impl<T: Config> Pallet<T> {
524		/// Place a bid.
525		///
526		/// Origin must be Signed, and account must have at least `amount` in free balance.
527		///
528		/// - `amount`: The amount of the bid; these funds will be reserved, and if/when
529		///   consolidated, removed. Must be at least `MinBid`.
530		/// - `duration`: The number of periods before which the newly consolidated bid may be
531		///   thawed. Must be greater than 1 and no more than `QueueCount`.
532		///
533		/// Complexities:
534		/// - `Queues[duration].len()` (just take max).
535		#[pallet::call_index(0)]
536		#[pallet::weight(T::WeightInfo::place_bid_max())]
537		pub fn place_bid(
538			origin: OriginFor<T>,
539			#[pallet::compact] amount: BalanceOf<T>,
540			duration: u32,
541		) -> DispatchResult {
542			let who = ensure_signed(origin)?;
543
544			ensure!(amount >= T::MinBid::get(), Error::<T>::AmountTooSmall);
545			let queue_count = T::QueueCount::get() as usize;
546			let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
547			ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
548
549			let net = Queues::<T>::try_mutate(
550				duration,
551				|q| -> Result<(u32, BalanceOf<T>), DispatchError> {
552					let queue_full = q.len() == T::MaxQueueLen::get() as usize;
553					ensure!(!queue_full || q[0].amount < amount, Error::<T>::BidTooLow);
554					T::Currency::hold(&HoldReason::NftReceipt.into(), &who, amount)?;
555
556					// queue is <Ordered: Lowest ... Highest><Fifo: Last ... First>
557					let mut bid = Bid { amount, who: who.clone() };
558					let net = if queue_full {
559						core::mem::swap(&mut q[0], &mut bid);
560						let _ = T::Currency::release(
561							&HoldReason::NftReceipt.into(),
562							&bid.who,
563							bid.amount,
564							BestEffort,
565						);
566						Self::deposit_event(Event::<T>::BidDropped {
567							who: bid.who,
568							amount: bid.amount,
569							duration,
570						});
571						(0, amount - bid.amount)
572					} else {
573						q.try_insert(0, bid).expect("verified queue was not full above. qed.");
574						(1, amount)
575					};
576
577					let sorted_item_count = q.len().saturating_sub(T::FifoQueueLen::get() as usize);
578					if sorted_item_count > 1 {
579						q[0..sorted_item_count].sort_by_key(|x| x.amount);
580					}
581
582					Ok(net)
583				},
584			)?;
585			QueueTotals::<T>::mutate(|qs| {
586				qs.bounded_resize(queue_count, (0, Zero::zero()));
587				qs[queue_index].0 += net.0;
588				qs[queue_index].1.saturating_accrue(net.1);
589			});
590			Self::deposit_event(Event::BidPlaced { who, amount, duration });
591
592			Ok(())
593		}
594
595		/// Retract a previously placed bid.
596		///
597		/// Origin must be Signed, and the account should have previously issued a still-active bid
598		/// of `amount` for `duration`.
599		///
600		/// - `amount`: The amount of the previous bid.
601		/// - `duration`: The duration of the previous bid.
602		#[pallet::call_index(1)]
603		#[pallet::weight(T::WeightInfo::retract_bid(T::MaxQueueLen::get()))]
604		pub fn retract_bid(
605			origin: OriginFor<T>,
606			#[pallet::compact] amount: BalanceOf<T>,
607			duration: u32,
608		) -> DispatchResult {
609			let who = ensure_signed(origin)?;
610
611			let queue_count = T::QueueCount::get() as usize;
612			let queue_index = duration.checked_sub(1).ok_or(Error::<T>::DurationTooSmall)? as usize;
613			ensure!(queue_index < queue_count, Error::<T>::DurationTooBig);
614
615			let bid = Bid { amount, who };
616
617			let mut queue = Queues::<T>::get(duration);
618			let pos = queue.iter().position(|i| i == &bid).ok_or(Error::<T>::UnknownBid)?;
619			queue.remove(pos);
620			let new_len = queue.len() as u32;
621
622			T::Currency::release(&HoldReason::NftReceipt.into(), &bid.who, bid.amount, BestEffort)?;
623
624			Queues::<T>::insert(duration, queue);
625			QueueTotals::<T>::mutate(|qs| {
626				qs.bounded_resize(queue_count, (0, Zero::zero()));
627				qs[queue_index].0 = new_len;
628				qs[queue_index].1.saturating_reduce(bid.amount);
629			});
630
631			Self::deposit_event(Event::BidRetracted { who: bid.who, amount: bid.amount, duration });
632
633			Ok(())
634		}
635
636		/// Ensure we have sufficient funding for all potential payouts.
637		///
638		/// - `origin`: Must be accepted by `FundOrigin`.
639		#[pallet::call_index(2)]
640		#[pallet::weight(T::WeightInfo::fund_deficit())]
641		pub fn fund_deficit(origin: OriginFor<T>) -> DispatchResult {
642			T::FundOrigin::ensure_origin(origin)?;
643			let summary: SummaryRecordOf<T> = Summary::<T>::get();
644			let our_account = Self::account_id();
645			let issuance = Self::issuance_with(&our_account, &summary);
646			let deficit = issuance.required.saturating_sub(issuance.holdings);
647			ensure!(!deficit.is_zero(), Error::<T>::AlreadyFunded);
648			T::Deficit::on_unbalanced(T::Currency::deposit(&our_account, deficit, Exact)?);
649			Self::deposit_event(Event::<T>::Funded { deficit });
650			Ok(())
651		}
652
653		/// Reduce or remove an outstanding receipt, placing the according proportion of funds into
654		/// the account of the owner.
655		///
656		/// - `origin`: Must be Signed and the account must be the owner of the receipt `index` as
657		///   well as any fungible counterpart.
658		/// - `index`: The index of the receipt.
659		/// - `portion`: If `Some`, then only the given portion of the receipt should be thawed. If
660		///   `None`, then all of it should be.
661		#[pallet::call_index(3)]
662		#[pallet::weight(T::WeightInfo::thaw_private())]
663		pub fn thaw_private(
664			origin: OriginFor<T>,
665			#[pallet::compact] index: ReceiptIndex,
666			maybe_proportion: Option<Perquintill>,
667		) -> DispatchResult {
668			let who = ensure_signed(origin)?;
669
670			// Look for `index`
671			let mut receipt: ReceiptRecordOf<T> =
672				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
673			// If found, check the owner is `who`.
674			let (owner, mut on_hold) = receipt.owner.ok_or(Error::<T>::AlreadyCommunal)?;
675			ensure!(owner == who, Error::<T>::NotOwner);
676
677			let now = frame_system::Pallet::<T>::block_number();
678			ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
679
680			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
681
682			let proportion = if let Some(proportion) = maybe_proportion {
683				ensure!(proportion <= receipt.proportion, Error::<T>::PortionTooBig);
684				let remaining = receipt.proportion.saturating_sub(proportion);
685				ensure!(
686					remaining.is_zero() || remaining >= T::MinReceipt::get(),
687					Error::<T>::MakesDust
688				);
689				proportion
690			} else {
691				receipt.proportion
692			};
693
694			let (throttle, throttle_period) = T::ThawThrottle::get();
695			if now.saturating_sub(summary.last_period) >= throttle_period {
696				summary.thawed = Zero::zero();
697				summary.last_period = now;
698			}
699			summary.thawed.saturating_accrue(proportion);
700			ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
701
702			// Multiply the proportion it is by the total issued.
703			let our_account = Self::account_id();
704			let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
705			//			let amount = proportion.mul_ceil(effective_issuance);
706			let amount = proportion * effective_issuance;
707
708			receipt.proportion.saturating_reduce(proportion);
709			summary.proportion_owed.saturating_reduce(proportion);
710
711			let dropped = receipt.proportion.is_zero();
712
713			if amount > on_hold {
714				T::Currency::release(&HoldReason::NftReceipt.into(), &who, on_hold, Exact)?;
715				let deficit = amount - on_hold;
716				// Try to transfer deficit from pot to receipt owner.
717				summary.receipts_on_hold.saturating_reduce(on_hold);
718				on_hold = Zero::zero();
719				T::Currency::transfer(&our_account, &who, deficit, Expendable)
720					.map_err(|_| Error::<T>::Unfunded)?;
721			} else {
722				on_hold.saturating_reduce(amount);
723				summary.receipts_on_hold.saturating_reduce(amount);
724				if dropped && !on_hold.is_zero() {
725					// Reclaim any remainder:
726					// Transfer excess of `on_hold` to the pot if we have now fully compensated for
727					// the receipt.
728					T::Currency::transfer_on_hold(
729						&HoldReason::NftReceipt.into(),
730						&who,
731						&our_account,
732						on_hold,
733						Exact,
734						Free,
735						Polite,
736					)
737					.map(|_| ())
738					// We ignore this error as it just means the amount we're trying to deposit is
739					// dust and the beneficiary account doesn't exist.
740					.or_else(|e| {
741						if e == TokenError::CannotCreate.into() {
742							Ok(())
743						} else {
744							Err(e)
745						}
746					})?;
747					summary.receipts_on_hold.saturating_reduce(on_hold);
748				}
749				T::Currency::release(&HoldReason::NftReceipt.into(), &who, amount, Exact)?;
750			}
751
752			if dropped {
753				Receipts::<T>::remove(index);
754			} else {
755				receipt.owner = Some((owner, on_hold));
756				Receipts::<T>::insert(index, &receipt);
757			}
758			Summary::<T>::put(&summary);
759
760			Self::deposit_event(Event::Thawed { index, who, amount, proportion, dropped });
761
762			Ok(())
763		}
764
765		/// Reduce or remove an outstanding receipt, placing the according proportion of funds into
766		/// the account of the owner.
767		///
768		/// - `origin`: Must be Signed and the account must be the owner of the fungible counterpart
769		///   for receipt `index`.
770		/// - `index`: The index of the receipt.
771		#[pallet::call_index(4)]
772		#[pallet::weight(T::WeightInfo::thaw_communal())]
773		pub fn thaw_communal(
774			origin: OriginFor<T>,
775			#[pallet::compact] index: ReceiptIndex,
776		) -> DispatchResult {
777			let who = ensure_signed(origin)?;
778
779			// Look for `index`
780			let receipt: ReceiptRecordOf<T> =
781				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
782			// If found, check it is actually communal.
783			ensure!(receipt.owner.is_none(), Error::<T>::NotOwner);
784			let now = frame_system::Pallet::<T>::block_number();
785			ensure!(now >= receipt.expiry, Error::<T>::NotExpired);
786
787			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
788
789			let (throttle, throttle_period) = T::ThawThrottle::get();
790			if now.saturating_sub(summary.last_period) >= throttle_period {
791				summary.thawed = Zero::zero();
792				summary.last_period = now;
793			}
794			summary.thawed.saturating_accrue(receipt.proportion);
795			ensure!(summary.thawed <= throttle, Error::<T>::Throttled);
796
797			let cp_amount = T::CounterpartAmount::convert(receipt.proportion);
798			T::Counterpart::burn_from(&who, cp_amount, Expendable, Exact, Polite)?;
799
800			// Multiply the proportion it is by the total issued.
801			let our_account = Self::account_id();
802			let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
803			let amount = receipt.proportion * effective_issuance;
804
805			summary.proportion_owed.saturating_reduce(receipt.proportion);
806
807			// Try to transfer amount owed from pot to receipt owner.
808			T::Currency::transfer(&our_account, &who, amount, Expendable)
809				.map_err(|_| Error::<T>::Unfunded)?;
810
811			Receipts::<T>::remove(index);
812			Summary::<T>::put(&summary);
813
814			let e =
815				Event::Thawed { index, who, amount, proportion: receipt.proportion, dropped: true };
816			Self::deposit_event(e);
817
818			Ok(())
819		}
820
821		/// Make a private receipt communal and create fungible counterparts for its owner.
822		#[pallet::call_index(5)]
823		#[pallet::weight(T::WeightInfo::communify())]
824		pub fn communify(
825			origin: OriginFor<T>,
826			#[pallet::compact] index: ReceiptIndex,
827		) -> DispatchResult {
828			let who = ensure_signed(origin)?;
829
830			// Look for `index`
831			let mut receipt: ReceiptRecordOf<T> =
832				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
833
834			// Check it's not already communal and make it so.
835			let (owner, on_hold) = receipt.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
836
837			// If found, check the owner is `who`.
838			ensure!(owner == who, Error::<T>::NotOwner);
839
840			// Unreserve and transfer the funds to the pot.
841			let reason = HoldReason::NftReceipt.into();
842			let us = Self::account_id();
843			T::Currency::transfer_on_hold(&reason, &who, &us, on_hold, Exact, Free, Polite)
844				.map_err(|_| Error::<T>::Unfunded)?;
845
846			// Record that we've moved the amount reserved.
847			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
848			summary.receipts_on_hold.saturating_reduce(on_hold);
849			Summary::<T>::put(&summary);
850			Receipts::<T>::insert(index, &receipt);
851
852			// Mint fungibles.
853			let fung_eq = T::CounterpartAmount::convert(receipt.proportion);
854			let _ = T::Counterpart::mint_into(&who, fung_eq).defensive();
855
856			Ok(())
857		}
858
859		/// Make a communal receipt private and burn fungible counterparts from its owner.
860		#[pallet::call_index(6)]
861		#[pallet::weight(T::WeightInfo::privatize())]
862		pub fn privatize(
863			origin: OriginFor<T>,
864			#[pallet::compact] index: ReceiptIndex,
865		) -> DispatchResult {
866			let who = ensure_signed(origin)?;
867
868			// Look for `index`
869			let mut receipt: ReceiptRecordOf<T> =
870				Receipts::<T>::get(index).ok_or(Error::<T>::UnknownReceipt)?;
871
872			// If found, check there is no owner.
873			ensure!(receipt.owner.is_none(), Error::<T>::AlreadyPrivate);
874
875			// Multiply the proportion it is by the total issued.
876			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
877			let our_account = Self::account_id();
878			let effective_issuance = Self::issuance_with(&our_account, &summary).effective;
879			let max_amount = receipt.proportion * effective_issuance;
880			// Avoid trying to place more in the account's reserve than we have available in the pot
881			let amount = max_amount.min(T::Currency::balance(&our_account));
882
883			// Burn fungible counterparts.
884			T::Counterpart::burn_from(
885				&who,
886				T::CounterpartAmount::convert(receipt.proportion),
887				Expendable,
888				Exact,
889				Polite,
890			)?;
891
892			// Transfer the funds from the pot to the owner and reserve
893			let reason = HoldReason::NftReceipt.into();
894			let us = Self::account_id();
895			T::Currency::transfer_and_hold(&reason, &us, &who, amount, Exact, Expendable, Polite)?;
896
897			// Record that we've moved the amount reserved.
898			summary.receipts_on_hold.saturating_accrue(amount);
899
900			receipt.owner = Some((who, amount));
901
902			Summary::<T>::put(&summary);
903			Receipts::<T>::insert(index, &receipt);
904
905			Ok(())
906		}
907	}
908
909	/// Issuance information returned by `issuance()`.
910	#[derive(Debug)]
911	pub struct IssuanceInfo<Balance> {
912		/// The balance held by this pallet instance together with the balances on hold across
913		/// all receipt-owning accounts.
914		pub holdings: Balance,
915		/// The (non-ignored) issuance in the system, not including this pallet's account.
916		pub other: Balance,
917		/// The effective total issuance, hypothetically if all outstanding receipts were thawed at
918		/// present.
919		pub effective: Balance,
920		/// The amount needed to be accessible to this pallet in case all outstanding receipts were
921		/// thawed at present. If it is more than `holdings`, then the pallet will need funding.
922		pub required: Balance,
923	}
924
925	impl<T: Config> NftInspect<T::AccountId> for Pallet<T> {
926		type ItemId = ReceiptIndex;
927
928		fn owner(item: &ReceiptIndex) -> Option<T::AccountId> {
929			Receipts::<T>::get(item).and_then(|r| r.owner).map(|(who, _)| who)
930		}
931
932		fn attribute(item: &Self::ItemId, key: &[u8]) -> Option<Vec<u8>> {
933			let item = Receipts::<T>::get(item)?;
934			match key {
935				b"proportion" => Some(item.proportion.encode()),
936				b"expiry" => Some(item.expiry.encode()),
937				b"owner" => item.owner.as_ref().map(|x| x.0.encode()),
938				b"on_hold" => item.owner.as_ref().map(|x| x.1.encode()),
939				_ => None,
940			}
941		}
942	}
943
944	impl<T: Config> NftTransfer<T::AccountId> for Pallet<T> {
945		fn transfer(index: &ReceiptIndex, dest: &T::AccountId) -> DispatchResult {
946			let mut item = Receipts::<T>::get(index).ok_or(TokenError::UnknownAsset)?;
947			let (owner, on_hold) = item.owner.take().ok_or(Error::<T>::AlreadyCommunal)?;
948
949			let reason = HoldReason::NftReceipt.into();
950			T::Currency::transfer_on_hold(&reason, &owner, dest, on_hold, Exact, OnHold, Polite)?;
951
952			item.owner = Some((dest.clone(), on_hold));
953			Receipts::<T>::insert(&index, &item);
954			Pallet::<T>::deposit_event(Event::<T>::Transferred {
955				from: owner,
956				to: dest.clone(),
957				index: *index,
958			});
959			Ok(())
960		}
961	}
962
963	impl<T: Config> Pallet<T> {
964		/// The account ID of the reserves.
965		///
966		/// This actually does computation. If you need to keep using it, then make sure you cache
967		/// the value and only call this once.
968		pub fn account_id() -> T::AccountId {
969			T::PalletId::get().into_account_truncating()
970		}
971
972		/// Returns information on the issuance within the system.
973		pub fn issuance() -> IssuanceInfo<BalanceOf<T>> {
974			Self::issuance_with(&Self::account_id(), &Summary::<T>::get())
975		}
976
977		/// Returns information on the issuance within the system
978		///
979		/// This function is equivalent to `issuance`, except that it accepts arguments rather than
980		/// queries state. If the arguments are already known, then this may be slightly more
981		/// performant.
982		///
983		/// - `our_account`: The value returned by `Self::account_id()`.
984		/// - `summary`: The value returned by `Summary::<T>::get()`.
985		pub fn issuance_with(
986			our_account: &T::AccountId,
987			summary: &SummaryRecordOf<T>,
988		) -> IssuanceInfo<BalanceOf<T>> {
989			let total_issuance =
990				T::Currency::active_issuance().saturating_sub(T::IgnoredIssuance::get());
991			let holdings =
992				T::Currency::balance(our_account).saturating_add(summary.receipts_on_hold);
993			let other = total_issuance.saturating_sub(holdings);
994			let effective =
995				summary.proportion_owed.left_from_one().saturating_reciprocal_mul(other);
996			let required = summary.proportion_owed * effective;
997			IssuanceInfo { holdings, other, effective, required }
998		}
999
1000		/// Process some bids into receipts up to a `target` total of all receipts.
1001		///
1002		/// Touch at most `max_queues`.
1003		///
1004		/// Return the weight used.
1005		pub(crate) fn process_queues(
1006			target: Perquintill,
1007			max_queues: u32,
1008			max_bids: u32,
1009			weight: &mut WeightCounter,
1010		) {
1011			let mut summary: SummaryRecordOf<T> = Summary::<T>::get();
1012			if summary.proportion_owed >= target {
1013				return
1014			}
1015
1016			let now = frame_system::Pallet::<T>::block_number();
1017			let our_account = Self::account_id();
1018			let issuance: IssuanceInfoOf<T> = Self::issuance_with(&our_account, &summary);
1019			let mut remaining = target.saturating_sub(summary.proportion_owed) * issuance.effective;
1020
1021			let mut queues_hit = 0;
1022			let mut bids_hit = 0;
1023			let mut totals = QueueTotals::<T>::get();
1024			let queue_count = T::QueueCount::get();
1025			totals.bounded_resize(queue_count as usize, (0, Zero::zero()));
1026			for duration in (1..=queue_count).rev() {
1027				if totals[duration as usize - 1].0.is_zero() {
1028					continue
1029				}
1030				if remaining.is_zero() || queues_hit >= max_queues
1031					|| !weight.check_accrue(T::WeightInfo::process_queue())
1032					// No point trying to process a queue if we can't process a single bid.
1033					|| !weight.can_accrue(T::WeightInfo::process_bid())
1034				{
1035					break
1036				}
1037
1038				let b = Self::process_queue(
1039					duration,
1040					now,
1041					&our_account,
1042					&issuance,
1043					max_bids.saturating_sub(bids_hit),
1044					&mut remaining,
1045					&mut totals[duration as usize - 1],
1046					&mut summary,
1047					weight,
1048				);
1049
1050				bids_hit.saturating_accrue(b);
1051				queues_hit.saturating_inc();
1052			}
1053			QueueTotals::<T>::put(&totals);
1054			Summary::<T>::put(&summary);
1055		}
1056
1057		pub(crate) fn process_queue(
1058			duration: u32,
1059			now: BlockNumberFor<T>,
1060			our_account: &T::AccountId,
1061			issuance: &IssuanceInfo<BalanceOf<T>>,
1062			max_bids: u32,
1063			remaining: &mut BalanceOf<T>,
1064			queue_total: &mut (u32, BalanceOf<T>),
1065			summary: &mut SummaryRecordOf<T>,
1066			weight: &mut WeightCounter,
1067		) -> u32 {
1068			let mut queue: BoundedVec<BidOf<T>, _> = Queues::<T>::get(&duration);
1069			let expiry = now.saturating_add(T::BasePeriod::get().saturating_mul(duration.into()));
1070			let mut count = 0;
1071
1072			while count < max_bids &&
1073				!queue.is_empty() &&
1074				!remaining.is_zero() &&
1075				weight.check_accrue(T::WeightInfo::process_bid())
1076			{
1077				let bid = match queue.pop() {
1078					Some(b) => b,
1079					None => break,
1080				};
1081				if let Some(bid) = Self::process_bid(
1082					bid,
1083					expiry,
1084					our_account,
1085					issuance,
1086					remaining,
1087					&mut queue_total.1,
1088					summary,
1089				) {
1090					queue.try_push(bid).expect("just popped, so there must be space. qed");
1091					// This should exit at the next iteration (though nothing will break if it
1092					// doesn't).
1093				}
1094				count.saturating_inc();
1095			}
1096			queue_total.0 = queue.len() as u32;
1097			Queues::<T>::insert(&duration, &queue);
1098			count
1099		}
1100
1101		pub(crate) fn process_bid(
1102			mut bid: BidOf<T>,
1103			expiry: BlockNumberFor<T>,
1104			_our_account: &T::AccountId,
1105			issuance: &IssuanceInfo<BalanceOf<T>>,
1106			remaining: &mut BalanceOf<T>,
1107			queue_amount: &mut BalanceOf<T>,
1108			summary: &mut SummaryRecordOf<T>,
1109		) -> Option<BidOf<T>> {
1110			let result = if *remaining < bid.amount {
1111				let overflow = bid.amount - *remaining;
1112				bid.amount = *remaining;
1113				Some(Bid { amount: overflow, who: bid.who.clone() })
1114			} else {
1115				None
1116			};
1117			let amount = bid.amount;
1118			summary.receipts_on_hold.saturating_accrue(amount);
1119
1120			// Can never overflow due to block above.
1121			remaining.saturating_reduce(amount);
1122			// Should never underflow since it should track the total of the
1123			// bids exactly, but we'll be defensive.
1124			queue_amount.defensive_saturating_reduce(amount);
1125
1126			// Now to activate the bid...
1127			let n = amount;
1128			let d = issuance.effective;
1129			let proportion = Perquintill::from_rational_with_rounding(n, d, Rounding::Down)
1130				.defensive_unwrap_or_default();
1131			let who = bid.who;
1132			let index = summary.index;
1133			summary.proportion_owed.defensive_saturating_accrue(proportion);
1134			summary.index += 1;
1135
1136			let e = Event::Issued { index, expiry, who: who.clone(), amount, proportion };
1137			Self::deposit_event(e);
1138			let receipt = ReceiptRecord { proportion, owner: Some((who, amount)), expiry };
1139			Receipts::<T>::insert(index, receipt);
1140
1141			result
1142		}
1143	}
1144}