referrerpolicy=no-referrer-when-downgrade

pallet_referenda/
types.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//! Miscellaneous additional datatypes.
19
20use super::*;
21use alloc::borrow::Cow;
22use codec::{Compact, Decode, DecodeWithMemTracking, Encode, EncodeLike, Input, MaxEncodedLen};
23use core::fmt::Debug;
24use frame_support::{
25	traits::{schedule::v3::Anon, Bounded},
26	Parameter,
27};
28use scale_info::{Type, TypeInfo};
29use sp_arithmetic::{Rounding::*, SignedRounding::*};
30use sp_runtime::{FixedI64, PerThing};
31
32pub type BalanceOf<T, I = ()> =
33	<<T as Config<I>>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
34
35pub type BlockNumberFor<T, I> =
36	<<T as Config<I>>::BlockNumberProvider as BlockNumberProvider>::BlockNumber;
37
38pub type NegativeImbalanceOf<T, I> = <<T as Config<I>>::Currency as Currency<
39	<T as frame_system::Config>::AccountId,
40>>::NegativeImbalance;
41pub type CallOf<T, I> = <T as Config<I>>::RuntimeCall;
42pub type BoundedCallOf<T, I> =
43	Bounded<<T as Config<I>>::RuntimeCall, <T as frame_system::Config>::Hashing>;
44pub type VotesOf<T, I> = <T as Config<I>>::Votes;
45pub type TallyOf<T, I> = <T as Config<I>>::Tally;
46pub type PalletsOriginOf<T> =
47	<<T as frame_system::Config>::RuntimeOrigin as OriginTrait>::PalletsOrigin;
48pub type ReferendumInfoOf<T, I> = ReferendumInfo<
49	TrackIdOf<T, I>,
50	PalletsOriginOf<T>,
51	BlockNumberFor<T, I>,
52	BoundedCallOf<T, I>,
53	BalanceOf<T, I>,
54	TallyOf<T, I>,
55	<T as frame_system::Config>::AccountId,
56	ScheduleAddressOf<T, I>,
57>;
58pub type ReferendumStatusOf<T, I> = ReferendumStatus<
59	TrackIdOf<T, I>,
60	PalletsOriginOf<T>,
61	BlockNumberFor<T, I>,
62	BoundedCallOf<T, I>,
63	BalanceOf<T, I>,
64	TallyOf<T, I>,
65	<T as frame_system::Config>::AccountId,
66	ScheduleAddressOf<T, I>,
67>;
68pub type DecidingStatusOf<T, I> = DecidingStatus<BlockNumberFor<T, I>>;
69pub type TrackInfoOf<T, I = ()> = TrackInfo<BalanceOf<T, I>, BlockNumberFor<T, I>>;
70pub type TrackIdOf<T, I> =
71	<<T as Config<I>>::Tracks as TracksInfo<BalanceOf<T, I>, BlockNumberFor<T, I>>>::Id;
72pub type ScheduleAddressOf<T, I> = <<T as Config<I>>::Scheduler as Anon<
73	BlockNumberFor<T, I>,
74	CallOf<T, I>,
75	PalletsOriginOf<T>,
76>>::Address;
77
78/// A referendum index.
79pub type ReferendumIndex = u32;
80
81pub trait InsertSorted<T> {
82	/// Inserts an item into a sorted series.
83	///
84	/// Returns `true` if it was inserted, `false` if it would belong beyond the bound of the
85	/// series.
86	fn insert_sorted_by_key<F: FnMut(&T) -> K, K: PartialOrd<K> + Ord>(
87		&mut self,
88		t: T,
89		f: F,
90	) -> bool;
91}
92impl<T: Ord, S: Get<u32>> InsertSorted<T> for BoundedVec<T, S> {
93	fn insert_sorted_by_key<F: FnMut(&T) -> K, K: PartialOrd<K> + Ord>(
94		&mut self,
95		t: T,
96		mut f: F,
97	) -> bool {
98		let index = self.binary_search_by_key::<K, F>(&f(&t), f).unwrap_or_else(|x| x);
99		self.force_insert_keep_right(index, t).is_ok()
100	}
101}
102
103#[derive(
104	Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
105)]
106pub struct DecidingStatus<BlockNumber> {
107	/// When this referendum began being "decided". If confirming, then the
108	/// end will actually be delayed until the end of the confirmation period.
109	pub since: BlockNumber,
110	/// If `Some`, then the referendum has entered confirmation stage and will end at
111	/// the block number as long as it doesn't lose its approval in the meantime.
112	pub confirming: Option<BlockNumber>,
113}
114
115#[derive(
116	Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
117)]
118pub struct Deposit<AccountId, Balance> {
119	pub who: AccountId,
120	pub amount: Balance,
121}
122
123pub const DEFAULT_MAX_TRACK_NAME_LEN: usize = 25;
124
125/// Helper structure to treat a `[u8; N]` array as a string.
126///
127/// This is a temporary fix (see [#7671](https://github.com/paritytech/polkadot-sdk/pull/7671)) in
128/// order to stop `polkadot.js` apps to fail when trying to decode the `name` field in `TrackInfo`.
129#[derive(Clone, Eq, DecodeWithMemTracking, PartialEq, Debug)]
130pub struct StringLike<const N: usize>(pub [u8; N]);
131
132impl<const N: usize> TypeInfo for StringLike<N> {
133	type Identity = <&'static str as TypeInfo>::Identity;
134
135	fn type_info() -> Type {
136		<&str as TypeInfo>::type_info()
137	}
138}
139
140impl<const N: usize> MaxEncodedLen for StringLike<N> {
141	fn max_encoded_len() -> usize {
142		<Compact<u32> as MaxEncodedLen>::max_encoded_len().saturating_add(N)
143	}
144}
145
146impl<const N: usize> Encode for StringLike<N> {
147	fn encode(&self) -> Vec<u8> {
148		use codec::Compact;
149		(Compact(N as u32), self.0).encode()
150	}
151}
152
153impl<const N: usize> Decode for StringLike<N> {
154	fn decode<I: Input>(input: &mut I) -> Result<Self, codec::Error> {
155		let Compact(size): Compact<u32> = Decode::decode(input)?;
156		if size != N as u32 {
157			return Err("Invalid size".into());
158		}
159
160		let bytes: [u8; N] = Decode::decode(input)?;
161		Ok(Self(bytes))
162	}
163}
164
165/// Detailed information about the configuration of a referenda track. Used for internal storage.
166pub type TrackInfo<Balance, Moment, const N: usize = DEFAULT_MAX_TRACK_NAME_LEN> =
167	TrackDetails<Balance, Moment, [u8; N]>;
168
169/// Detailed information about the configuration of a referenda track. Used for const querying.
170pub type ConstTrackInfo<Balance, Moment, const N: usize = DEFAULT_MAX_TRACK_NAME_LEN> =
171	TrackDetails<Balance, Moment, StringLike<N>>;
172
173/// Detailed information about the configuration of a referenda track
174#[derive(
175	Clone, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Eq, PartialEq, Debug,
176)]
177pub struct TrackDetails<Balance, Moment, Name> {
178	/// Name of this track.
179	pub name: Name,
180	/// A limit for the number of referenda on this track that can be being decided at once.
181	/// For Root origin this should generally be just one.
182	pub max_deciding: u32,
183	/// Amount that must be placed on deposit before a decision can be made.
184	pub decision_deposit: Balance,
185	/// Amount of time this must be submitted for before a decision can be made.
186	pub prepare_period: Moment,
187	/// Amount of time that a decision may take to be approved prior to cancellation.
188	pub decision_period: Moment,
189	/// Amount of time that the approval criteria must hold before it can be approved.
190	pub confirm_period: Moment,
191	/// Minimum amount of time that an approved proposal must be in the dispatch queue.
192	pub min_enactment_period: Moment,
193	/// Minimum aye votes as percentage of overall conviction-weighted votes needed for
194	/// approval as a function of time into decision period.
195	pub min_approval: Curve,
196	/// Minimum pre-conviction aye-votes ("support") as percentage of overall population that is
197	/// needed for approval as a function of time into decision period.
198	pub min_support: Curve,
199}
200
201/// Track groups the information of a voting track with its corresponding identifier
202#[derive(
203	Clone, Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Eq, PartialEq, Debug,
204)]
205pub struct Track<Id, Balance, Moment, const N: usize = DEFAULT_MAX_TRACK_NAME_LEN> {
206	pub id: Id,
207	pub info: TrackInfo<Balance, Moment, N>,
208}
209
210/// Information on the voting tracks.
211pub trait TracksInfo<Balance, Moment, const N: usize = DEFAULT_MAX_TRACK_NAME_LEN>
212where
213	Balance: Clone + Debug + Eq + 'static,
214	Moment: Clone + Debug + Eq + 'static,
215{
216	/// The identifier for a track.
217	type Id: Copy + Parameter + Ord + PartialOrd + Send + Sync + 'static + MaxEncodedLen;
218
219	/// The origin type from which a track is implied.
220	type RuntimeOrigin;
221
222	/// Return the sorted iterable list of known tracks and their information.
223	///
224	/// The iterator MUST be sorted by `Id`. Consumers of this trait are advised to assert
225	/// [`Self::check_integrity`] prior to any use.
226	fn tracks() -> impl Iterator<Item = Cow<'static, Track<Self::Id, Balance, Moment, N>>>;
227
228	/// Determine the voting track for the given `origin`.
229	fn track_for(origin: &Self::RuntimeOrigin) -> Result<Self::Id, ()>;
230
231	/// Return the list of identifiers of the known tracks.
232	fn track_ids() -> impl Iterator<Item = Self::Id> {
233		Self::tracks().map(|x| x.id)
234	}
235
236	/// Return the track info for track `id`, by default this just looks it up in `Self::tracks()`.
237	fn info(id: Self::Id) -> Option<Cow<'static, TrackInfo<Balance, Moment, N>>> {
238		Self::tracks().find(|x| x.id == id).map(|t| match t {
239			Cow::Borrowed(x) => Cow::Borrowed(&x.info),
240			Cow::Owned(x) => Cow::Owned(x.info),
241		})
242	}
243
244	/// Check assumptions about the static data that this trait provides.
245	fn check_integrity() -> Result<(), &'static str> {
246		use core::cmp::Ordering;
247		// Adapted from Iterator::is_sorted implementation available in nightly
248		// https://github.com/rust-lang/rust/issues/53485
249		let mut iter = Self::tracks();
250		let mut last = match iter.next() {
251			Some(ref e) => e.id,
252			None => return Ok(()),
253		};
254		iter.all(|curr| {
255			let curr = curr.as_ref().id;
256			if let Ordering::Greater = last.cmp(&curr) {
257				return false;
258			}
259			last = curr;
260			true
261		})
262		.then_some(())
263		.ok_or("The tracks that were returned by `tracks` were not sorted by `Id`")
264	}
265}
266
267/// Info regarding an ongoing referendum.
268#[derive(
269	Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
270)]
271pub struct ReferendumStatus<
272	TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
273	RuntimeOrigin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
274	Moment: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike,
275	Call: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
276	Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
277	Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
278	AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
279	ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
280> {
281	/// The track of this referendum.
282	pub track: TrackId,
283	/// The origin for this referendum.
284	pub origin: RuntimeOrigin,
285	/// The hash of the proposal up for referendum.
286	pub proposal: Call,
287	/// The time the proposal should be scheduled for enactment.
288	pub enactment: DispatchTime<Moment>,
289	/// The time of submission. Once `UndecidingTimeout` passes, it may be closed by anyone if
290	/// `deciding` is `None`.
291	pub submitted: Moment,
292	/// The deposit reserved for the submission of this referendum.
293	pub submission_deposit: Deposit<AccountId, Balance>,
294	/// The deposit reserved for this referendum to be decided.
295	pub decision_deposit: Option<Deposit<AccountId, Balance>>,
296	/// The status of a decision being made. If `None`, it has not entered the deciding period.
297	pub deciding: Option<DecidingStatus<Moment>>,
298	/// The current tally of votes in this referendum.
299	pub tally: Tally,
300	/// Whether we have been placed in the queue for being decided or not.
301	pub in_queue: bool,
302	/// The next scheduled wake-up, if `Some`.
303	pub alarm: Option<(Moment, ScheduleAddress)>,
304}
305
306/// Info regarding a referendum, present or past.
307#[derive(
308	Encode, Decode, DecodeWithMemTracking, Clone, PartialEq, Eq, Debug, TypeInfo, MaxEncodedLen,
309)]
310pub enum ReferendumInfo<
311	TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
312	RuntimeOrigin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
313	Moment: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike,
314	Call: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
315	Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
316	Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
317	AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
318	ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
319> {
320	/// Referendum has been submitted and is being voted on.
321	Ongoing(
322		ReferendumStatus<
323			TrackId,
324			RuntimeOrigin,
325			Moment,
326			Call,
327			Balance,
328			Tally,
329			AccountId,
330			ScheduleAddress,
331		>,
332	),
333	/// Referendum finished with approval. Submission deposit is held.
334	Approved(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
335	/// Referendum finished with rejection. Submission deposit is held.
336	Rejected(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
337	/// Referendum finished with cancellation. Submission deposit is held.
338	Cancelled(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
339	/// Referendum finished and was never decided. Submission deposit is held.
340	TimedOut(Moment, Option<Deposit<AccountId, Balance>>, Option<Deposit<AccountId, Balance>>),
341	/// Referendum finished with a kill.
342	Killed(Moment),
343}
344
345impl<
346		TrackId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
347		RuntimeOrigin: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
348		Moment: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone + EncodeLike,
349		Call: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
350		Balance: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
351		Tally: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
352		AccountId: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
353		ScheduleAddress: Eq + PartialEq + Debug + Encode + Decode + TypeInfo + Clone,
354	>
355	ReferendumInfo<TrackId, RuntimeOrigin, Moment, Call, Balance, Tally, AccountId, ScheduleAddress>
356{
357	/// Take the Decision Deposit from `self`, if there is one. Returns an `Err` if `self` is not
358	/// in a valid state for the Decision Deposit to be refunded.
359	pub fn take_decision_deposit(&mut self) -> Result<Option<Deposit<AccountId, Balance>>, ()> {
360		use ReferendumInfo::*;
361		match self {
362			Ongoing(x) if x.decision_deposit.is_none() => Ok(None),
363			// Cannot refund deposit if Ongoing as this breaks assumptions.
364			Ongoing(_) => Err(()),
365			Approved(_, _, d) | Rejected(_, _, d) | TimedOut(_, _, d) | Cancelled(_, _, d) =>
366				Ok(d.take()),
367			Killed(_) => Ok(None),
368		}
369	}
370
371	/// Take the Submission Deposit from `self`, if there is one and it's in a valid state to be
372	/// taken. Returns an `Err` if `self` is not in a valid state for the Submission Deposit to be
373	/// refunded.
374	pub fn take_submission_deposit(&mut self) -> Result<Option<Deposit<AccountId, Balance>>, ()> {
375		use ReferendumInfo::*;
376		match self {
377			// Can only refund deposit if it's approved or cancelled.
378			Approved(_, s, _) | Cancelled(_, s, _) => Ok(s.take()),
379			// Cannot refund deposit if Ongoing as this breaks assumptions.
380			Ongoing(..) | Rejected(..) | TimedOut(..) | Killed(..) => Err(()),
381		}
382	}
383}
384
385/// Type for describing a curve over the 2-dimensional space of axes between 0-1, as represented
386/// by `(Perbill, Perbill)`.
387#[derive(Clone, Eq, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen)]
388#[cfg_attr(not(feature = "std"), derive(Debug))]
389pub enum Curve {
390	/// Linear curve starting at `(0, ceil)`, proceeding linearly to `(length, floor)`, then
391	/// remaining at `floor` until the end of the period.
392	LinearDecreasing { length: Perbill, floor: Perbill, ceil: Perbill },
393	/// Stepped curve, beginning at `(0, begin)`, then remaining constant for `period`, at which
394	/// point it steps down to `(period, begin - step)`. It then remains constant for another
395	/// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues
396	/// but the `y` component has a lower limit of `end`.
397	SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill },
398	/// A recipocal (`K/(x+S)-T`) curve: `factor` is `K` and `x_offset` is `S`, `y_offset` is `T`.
399	Reciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 },
400}
401
402/// Calculate the quadratic solution for the given curve.
403///
404/// WARNING: This is a `const` function designed for convenient use at build time and
405/// will panic on overflow. Ensure that any inputs are sensible.
406const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 {
407	const TWO: FixedI64 = FixedI64::from_u32(2);
408	const FOUR: FixedI64 = FixedI64::from_u32(4);
409	b.neg().add(b.mul(b).sub(FOUR.mul(a).mul(c)).sqrt()).div(TWO.mul(a))
410}
411
412impl Curve {
413	/// Create a `Curve::Linear` instance from a high-level description.
414	///
415	/// WARNING: This is a `const` function designed for convenient use at build time and
416	/// will panic on overflow. Ensure that any inputs are sensible.
417	pub const fn make_linear(length: u128, period: u128, floor: FixedI64, ceil: FixedI64) -> Curve {
418		let length = FixedI64::from_rational(length, period).into_perbill();
419		let floor = floor.into_perbill();
420		let ceil = ceil.into_perbill();
421		Curve::LinearDecreasing { length, floor, ceil }
422	}
423
424	/// Create a `Curve::Reciprocal` instance from a high-level description.
425	///
426	/// WARNING: This is a `const` function designed for convenient use at build time and
427	/// will panic on overflow. Ensure that any inputs are sensible.
428	pub const fn make_reciprocal(
429		delay: u128,
430		period: u128,
431		level: FixedI64,
432		floor: FixedI64,
433		ceil: FixedI64,
434	) -> Curve {
435		let delay = FixedI64::from_rational(delay, period).into_perbill();
436		let mut bounds = (
437			(
438				FixedI64::from_u32(0),
439				Self::reciprocal_from_parts(FixedI64::from_u32(0), floor, ceil),
440				FixedI64::from_inner(i64::max_value()),
441			),
442			(
443				FixedI64::from_u32(1),
444				Self::reciprocal_from_parts(FixedI64::from_u32(1), floor, ceil),
445				FixedI64::from_inner(i64::max_value()),
446			),
447		);
448		const TWO: FixedI64 = FixedI64::from_u32(2);
449		while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 {
450			let factor = (bounds.0).0.add((bounds.1).0).div(TWO);
451			let curve = Self::reciprocal_from_parts(factor, floor, ceil);
452			let curve_level = FixedI64::from_perbill(curve.const_threshold(delay));
453			if curve_level.into_inner() > level.into_inner() {
454				bounds = (bounds.0, (factor, curve, curve_level.sub(level)));
455			} else {
456				bounds = ((factor, curve, level.sub(curve_level)), bounds.1);
457			}
458		}
459		if (bounds.0).2.into_inner() < (bounds.1).2.into_inner() {
460			(bounds.0).1
461		} else {
462			(bounds.1).1
463		}
464	}
465
466	/// Create a `Curve::Reciprocal` instance from basic parameters.
467	///
468	/// WARNING: This is a `const` function designed for convenient use at build time and
469	/// will panic on overflow. Ensure that any inputs are sensible.
470	const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64, ceil: FixedI64) -> Self {
471		let delta = ceil.sub(floor);
472		let x_offset = pos_quad_solution(delta, delta, factor.neg());
473		let y_offset = floor.sub(factor.div(FixedI64::from_u32(1).add(x_offset)));
474		Curve::Reciprocal { factor, x_offset, y_offset }
475	}
476
477	/// Print some info on the curve.
478	#[cfg(feature = "std")]
479	pub fn info(&self, days: u32, name: impl std::fmt::Display) {
480		let hours = days * 24;
481		println!("Curve {} := {:?}:", name, self);
482		println!("   t + 0h:   {:?}", self.threshold(Perbill::zero()));
483		println!("   t + 1h:   {:?}", self.threshold(Perbill::from_rational(1, hours)));
484		println!("   t + 2h:   {:?}", self.threshold(Perbill::from_rational(2, hours)));
485		println!("   t + 3h:   {:?}", self.threshold(Perbill::from_rational(3, hours)));
486		println!("   t + 6h:   {:?}", self.threshold(Perbill::from_rational(6, hours)));
487		println!("   t + 12h:  {:?}", self.threshold(Perbill::from_rational(12, hours)));
488		println!("   t + 24h:  {:?}", self.threshold(Perbill::from_rational(24, hours)));
489		let mut l = 0;
490		for &(n, d) in [(1, 12), (1, 8), (1, 4), (1, 2), (3, 4), (1, 1)].iter() {
491			let t = days * n / d;
492			if t != l {
493				println!("   t + {}d:   {:?}", t, self.threshold(Perbill::from_rational(t, days)));
494				l = t;
495			}
496		}
497		let t = |p: Perbill| -> std::string::String {
498			if p.is_one() {
499				"never".into()
500			} else {
501				let minutes = p * (hours * 60);
502				if minutes < 60 {
503					format!("{} minutes", minutes)
504				} else if minutes < 8 * 60 && minutes % 60 != 0 {
505					format!("{} hours {} minutes", minutes / 60, minutes % 60)
506				} else if minutes < 72 * 60 {
507					format!("{} hours", minutes / 60)
508				} else if minutes / 60 % 24 == 0 {
509					format!("{} days", minutes / 60 / 24)
510				} else {
511					format!("{} days {} hours", minutes / 60 / 24, minutes / 60 % 24)
512				}
513			}
514		};
515		if self.delay(Perbill::from_percent(49)) < Perbill::one() {
516			println!("   30% threshold:   {}", t(self.delay(Perbill::from_percent(30))));
517			println!("   10% threshold:   {}", t(self.delay(Perbill::from_percent(10))));
518			println!("   3% threshold:    {}", t(self.delay(Perbill::from_percent(3))));
519			println!("   1% threshold:    {}", t(self.delay(Perbill::from_percent(1))));
520			println!("   0.1% threshold:  {}", t(self.delay(Perbill::from_rational(1u32, 1_000))));
521			println!("   0.01% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 10_000))));
522		} else {
523			println!(
524				"   99.9% threshold: {}",
525				t(self.delay(Perbill::from_rational(999u32, 1_000)))
526			);
527			println!("   99% threshold:   {}", t(self.delay(Perbill::from_percent(99))));
528			println!("   95% threshold:   {}", t(self.delay(Perbill::from_percent(95))));
529			println!("   90% threshold:   {}", t(self.delay(Perbill::from_percent(90))));
530			println!("   75% threshold:   {}", t(self.delay(Perbill::from_percent(75))));
531			println!("   60% threshold:   {}", t(self.delay(Perbill::from_percent(60))));
532		}
533	}
534
535	/// Determine the `y` value for the given `x` value.
536	pub fn threshold(&self, x: Perbill) -> Perbill {
537		match self {
538			Self::LinearDecreasing { length, floor, ceil } =>
539				*ceil - (x.min(*length).saturating_div(*length, Down) * (*ceil - *floor)),
540			Self::SteppedDecreasing { begin, end, step, period } =>
541				(*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end),
542			Self::Reciprocal { factor, x_offset, y_offset } => factor
543				.checked_rounding_div(FixedI64::from(x) + *x_offset, Low)
544				.map(|yp| (yp + *y_offset).into_clamped_perthing())
545				.unwrap_or_else(Perbill::one),
546		}
547	}
548
549	/// Determine the `y` value for the given `x` value.
550	///
551	/// This is a partial implementation designed only for use in const functions.
552	const fn const_threshold(&self, x: Perbill) -> Perbill {
553		match self {
554			Self::Reciprocal { factor, x_offset, y_offset } => {
555				match factor.checked_rounding_div(FixedI64::from_perbill(x).add(*x_offset), Low) {
556					Some(yp) => (yp.add(*y_offset)).into_perbill(),
557					None => Perbill::one(),
558				}
559			},
560			_ => panic!("const_threshold cannot be used on this curve"),
561		}
562	}
563
564	/// Determine the smallest `x` value such that `passing` returns `true` when passed along with
565	/// the given `y` value.
566	///
567	/// If `passing` never returns `true` for any value of `x` when paired with `y`, then
568	/// `Perbill::one` may be returned.
569	///
570	/// ```nocompile
571	/// let c = Curve::LinearDecreasing { begin: Perbill::one(), delta: Perbill::one() };
572	/// //      ^^^ Can be any curve.
573	/// let y = Perbill::from_percent(50);
574	/// //      ^^^ Can be any value.
575	/// let x = c.delay(y);
576	/// assert!(c.passing(x, y));
577	/// ```
578	pub fn delay(&self, y: Perbill) -> Perbill {
579		match self {
580			Self::LinearDecreasing { length, floor, ceil } =>
581				if y < *floor {
582					Perbill::one()
583				} else if y > *ceil {
584					Perbill::zero()
585				} else {
586					(*ceil - y).saturating_div(*ceil - *floor, Up).saturating_mul(*length)
587				},
588			Self::SteppedDecreasing { begin, end, step, period } =>
589				if y < *end {
590					Perbill::one()
591				} else {
592					period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step))
593				},
594			Self::Reciprocal { factor, x_offset, y_offset } => {
595				let y = FixedI64::from(y);
596				let maybe_term = factor.checked_rounding_div(y - *y_offset, High);
597				maybe_term
598					.and_then(|term| (term - *x_offset).try_into_perthing().ok())
599					.unwrap_or_else(Perbill::one)
600			},
601		}
602	}
603
604	/// Return `true` iff the `y` value is greater than the curve at the `x`.
605	pub fn passing(&self, x: Perbill, y: Perbill) -> bool {
606		y >= self.threshold(x)
607	}
608}
609
610#[cfg(feature = "std")]
611impl Debug for Curve {
612	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
613		match self {
614			Self::LinearDecreasing { length, floor, ceil } => {
615				write!(
616					f,
617					"Linear[(0%, {:?}) -> ({:?}, {:?}) -> (100%, {:?})]",
618					ceil, length, floor, floor,
619				)
620			},
621			Self::SteppedDecreasing { begin, end, step, period } => {
622				write!(
623					f,
624					"Stepped[(0%, {:?}) -> (100%, {:?}) by ({:?}, {:?})]",
625					begin, end, period, step,
626				)
627			},
628			Self::Reciprocal { factor, x_offset, y_offset } => {
629				write!(
630					f,
631					"Reciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]",
632					factor, x_offset, y_offset,
633				)
634			},
635		}
636	}
637}
638
639#[cfg(test)]
640mod tests {
641	use super::*;
642	use frame_support::traits::ConstU32;
643	use sp_runtime::{str_array as s, PerThing};
644
645	const fn percent(x: u128) -> FixedI64 {
646		FixedI64::from_rational(x, 100)
647	}
648
649	const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100));
650	const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0), percent(50));
651	const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100));
652	const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0), percent(50));
653	const WHITE_APP: Curve =
654		Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50), percent(100));
655	const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10), percent(50));
656	const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100));
657	const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0), percent(50));
658	const MID_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100));
659	const MID_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50));
660	const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50), percent(100));
661	const BIG_SUP: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0), percent(50));
662	const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50), percent(100));
663	const HUGE_SUP: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0), percent(50));
664	const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100));
665	const PARAM_SUP: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0), percent(50));
666	const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100));
667	const ADMIN_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50));
668
669	// TODO: ceil for linear.
670
671	#[test]
672	#[should_panic]
673	fn check_curves() {
674		TIP_APP.info(28u32, "Tip Approval");
675		TIP_SUP.info(28u32, "Tip Support");
676		ROOT_APP.info(28u32, "Root Approval");
677		ROOT_SUP.info(28u32, "Root Support");
678		WHITE_APP.info(28u32, "Whitelist Approval");
679		WHITE_SUP.info(28u32, "Whitelist Support");
680		SMALL_APP.info(28u32, "Small Spend Approval");
681		SMALL_SUP.info(28u32, "Small Spend Support");
682		MID_APP.info(28u32, "Mid Spend Approval");
683		MID_SUP.info(28u32, "Mid Spend Support");
684		BIG_APP.info(28u32, "Big Spend Approval");
685		BIG_SUP.info(28u32, "Big Spend Support");
686		HUGE_APP.info(28u32, "Huge Spend Approval");
687		HUGE_SUP.info(28u32, "Huge Spend Support");
688		PARAM_APP.info(28u32, "Mid-tier Parameter Change Approval");
689		PARAM_SUP.info(28u32, "Mid-tier Parameter Change Support");
690		ADMIN_APP.info(28u32, "Admin (e.g. Cancel Slash) Approval");
691		ADMIN_SUP.info(28u32, "Admin (e.g. Cancel Slash) Support");
692		assert!(false);
693	}
694
695	#[test]
696	fn insert_sorted_works() {
697		let mut b: BoundedVec<u32, ConstU32<6>> = vec![20, 30, 40].try_into().unwrap();
698		assert!(b.insert_sorted_by_key(10, |&x| x));
699		assert_eq!(&b[..], &[10, 20, 30, 40][..]);
700
701		assert!(b.insert_sorted_by_key(60, |&x| x));
702		assert_eq!(&b[..], &[10, 20, 30, 40, 60][..]);
703
704		assert!(b.insert_sorted_by_key(50, |&x| x));
705		assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]);
706
707		assert!(!b.insert_sorted_by_key(9, |&x| x));
708		assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]);
709
710		assert!(b.insert_sorted_by_key(11, |&x| x));
711		assert_eq!(&b[..], &[11, 20, 30, 40, 50, 60][..]);
712
713		assert!(b.insert_sorted_by_key(21, |&x| x));
714		assert_eq!(&b[..], &[20, 21, 30, 40, 50, 60][..]);
715
716		assert!(b.insert_sorted_by_key(61, |&x| x));
717		assert_eq!(&b[..], &[21, 30, 40, 50, 60, 61][..]);
718
719		assert!(b.insert_sorted_by_key(51, |&x| x));
720		assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]);
721	}
722
723	#[test]
724	fn translated_reciprocal_works() {
725		let c: Curve = Curve::Reciprocal {
726			factor: FixedI64::from_float(0.03125),
727			x_offset: FixedI64::from_float(0.0363306838226),
728			y_offset: FixedI64::from_float(0.139845532427),
729		};
730		c.info(28u32, "Test");
731
732		for i in 0..9_696_969u32 {
733			let query = Perbill::from_rational(i, 9_696_969);
734			// Determine the nearest point in time when the query will be above threshold.
735			let delay_needed = c.delay(query);
736			// Ensure that it actually does pass at that time, or that it will never pass.
737			assert!(delay_needed.is_one() || c.passing(delay_needed, query));
738		}
739	}
740
741	#[test]
742	fn stepped_decreasing_works() {
743		fn pc(x: u32) -> Perbill {
744			Perbill::from_percent(x)
745		}
746
747		let c =
748			Curve::SteppedDecreasing { begin: pc(80), end: pc(30), step: pc(10), period: pc(15) };
749
750		for i in 0..9_696_969u32 {
751			let query = Perbill::from_rational(i, 9_696_969);
752			// Determine the nearest point in time when the query will be above threshold.
753			let delay_needed = c.delay(query);
754			// Ensure that it actually does pass at that time, or that it will never pass.
755			assert!(delay_needed.is_one() || c.passing(delay_needed, query));
756		}
757
758		assert_eq!(c.threshold(pc(0)), pc(80));
759		assert_eq!(c.threshold(pc(15).less_epsilon()), pc(80));
760		assert_eq!(c.threshold(pc(15)), pc(70));
761		assert_eq!(c.threshold(pc(30).less_epsilon()), pc(70));
762		assert_eq!(c.threshold(pc(30)), pc(60));
763		assert_eq!(c.threshold(pc(45).less_epsilon()), pc(60));
764		assert_eq!(c.threshold(pc(45)), pc(50));
765		assert_eq!(c.threshold(pc(60).less_epsilon()), pc(50));
766		assert_eq!(c.threshold(pc(60)), pc(40));
767		assert_eq!(c.threshold(pc(75).less_epsilon()), pc(40));
768		assert_eq!(c.threshold(pc(75)), pc(30));
769		assert_eq!(c.threshold(pc(100)), pc(30));
770
771		assert_eq!(c.delay(pc(100)), pc(0));
772		assert_eq!(c.delay(pc(80)), pc(0));
773		assert_eq!(c.delay(pc(80).less_epsilon()), pc(15));
774		assert_eq!(c.delay(pc(70)), pc(15));
775		assert_eq!(c.delay(pc(70).less_epsilon()), pc(30));
776		assert_eq!(c.delay(pc(60)), pc(30));
777		assert_eq!(c.delay(pc(60).less_epsilon()), pc(45));
778		assert_eq!(c.delay(pc(50)), pc(45));
779		assert_eq!(c.delay(pc(50).less_epsilon()), pc(60));
780		assert_eq!(c.delay(pc(40)), pc(60));
781		assert_eq!(c.delay(pc(40).less_epsilon()), pc(75));
782		assert_eq!(c.delay(pc(30)), pc(75));
783		assert_eq!(c.delay(pc(30).less_epsilon()), pc(100));
784		assert_eq!(c.delay(pc(0)), pc(100));
785	}
786
787	#[test]
788	fn tracks_integrity_check_detects_unsorted() {
789		use crate::mock::RuntimeOrigin;
790
791		pub struct BadTracksInfo;
792		impl TracksInfo<u64, u64> for BadTracksInfo {
793			type Id = u8;
794			type RuntimeOrigin = <RuntimeOrigin as OriginTrait>::PalletsOrigin;
795			fn tracks() -> impl Iterator<Item = Cow<'static, Track<Self::Id, u64, u64>>> {
796				static DATA: [Track<u8, u64, u64>; 2] = [
797					Track {
798						id: 1u8,
799						info: TrackInfo {
800							name: s("root"),
801							max_deciding: 1,
802							decision_deposit: 10,
803							prepare_period: 4,
804							decision_period: 4,
805							confirm_period: 2,
806							min_enactment_period: 4,
807							min_approval: Curve::LinearDecreasing {
808								length: Perbill::from_percent(100),
809								floor: Perbill::from_percent(50),
810								ceil: Perbill::from_percent(100),
811							},
812							min_support: Curve::LinearDecreasing {
813								length: Perbill::from_percent(100),
814								floor: Perbill::from_percent(0),
815								ceil: Perbill::from_percent(100),
816							},
817						},
818					},
819					Track {
820						id: 0u8,
821						info: TrackInfo {
822							name: s("none"),
823							max_deciding: 3,
824							decision_deposit: 1,
825							prepare_period: 2,
826							decision_period: 2,
827							confirm_period: 1,
828							min_enactment_period: 2,
829							min_approval: Curve::LinearDecreasing {
830								length: Perbill::from_percent(100),
831								floor: Perbill::from_percent(95),
832								ceil: Perbill::from_percent(100),
833							},
834							min_support: Curve::LinearDecreasing {
835								length: Perbill::from_percent(100),
836								floor: Perbill::from_percent(90),
837								ceil: Perbill::from_percent(100),
838							},
839						},
840					},
841				];
842				DATA.iter().map(Cow::Borrowed)
843			}
844			fn track_for(_: &Self::RuntimeOrigin) -> Result<Self::Id, ()> {
845				unimplemented!()
846			}
847		}
848
849		assert_eq!(
850			BadTracksInfo::check_integrity(),
851			Err("The tracks that were returned by `tracks` were not sorted by `Id`")
852		);
853	}
854
855	#[test]
856	fn encoding_and_decoding_of_string_like_structure_works() {
857		let string_like = StringLike::<13>(*b"hello, world!");
858		let encoded: Vec<u8> = string_like.encode();
859
860		let decoded_as_vec: Vec<u8> =
861			Decode::decode(&mut &encoded.clone()[..]).expect("decoding as Vec<u8> should work");
862		assert_eq!(decoded_as_vec.len(), 13);
863		let decoded_as_str: alloc::string::String =
864			Decode::decode(&mut &encoded.clone()[..]).expect("decoding as str should work");
865		assert_eq!(decoded_as_str.len(), 13);
866		let decoded_as_string_like: StringLike<13> =
867			Decode::decode(&mut &encoded.clone()[..]).expect("decoding as StringLike should work");
868		assert_eq!(decoded_as_string_like.0.len(), 13);
869	}
870}