Skip to main content

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			},
368			Killed(_) => Ok(None),
369		}
370	}
371
372	/// Take the Submission Deposit from `self`, if there is one and it's in a valid state to be
373	/// taken. Returns an `Err` if `self` is not in a valid state for the Submission Deposit to be
374	/// refunded.
375	pub fn take_submission_deposit(&mut self) -> Result<Option<Deposit<AccountId, Balance>>, ()> {
376		use ReferendumInfo::*;
377		match self {
378			// Can only refund deposit if it's approved or cancelled.
379			Approved(_, s, _) | Cancelled(_, s, _) => Ok(s.take()),
380			// Cannot refund deposit if Ongoing as this breaks assumptions.
381			Ongoing(..) | Rejected(..) | TimedOut(..) | Killed(..) => Err(()),
382		}
383	}
384}
385
386/// Type for describing a curve over the 2-dimensional space of axes between 0-1, as represented
387/// by `(Perbill, Perbill)`.
388#[derive(Clone, Eq, PartialEq, Encode, Decode, DecodeWithMemTracking, TypeInfo, MaxEncodedLen)]
389#[cfg_attr(not(feature = "std"), derive(Debug))]
390pub enum Curve {
391	/// Linear curve starting at `(0, ceil)`, proceeding linearly to `(length, floor)`, then
392	/// remaining at `floor` until the end of the period.
393	LinearDecreasing { length: Perbill, floor: Perbill, ceil: Perbill },
394	/// Stepped curve, beginning at `(0, begin)`, then remaining constant for `period`, at which
395	/// point it steps down to `(period, begin - step)`. It then remains constant for another
396	/// `period` before stepping down to `(period * 2, begin - step * 2)`. This pattern continues
397	/// but the `y` component has a lower limit of `end`.
398	SteppedDecreasing { begin: Perbill, end: Perbill, step: Perbill, period: Perbill },
399	/// A recipocal (`K/(x+S)-T`) curve: `factor` is `K` and `x_offset` is `S`, `y_offset` is `T`.
400	Reciprocal { factor: FixedI64, x_offset: FixedI64, y_offset: FixedI64 },
401}
402
403/// Calculate the quadratic solution for the given curve.
404///
405/// WARNING: This is a `const` function designed for convenient use at build time and
406/// will panic on overflow. Ensure that any inputs are sensible.
407const fn pos_quad_solution(a: FixedI64, b: FixedI64, c: FixedI64) -> FixedI64 {
408	const TWO: FixedI64 = FixedI64::from_u32(2);
409	const FOUR: FixedI64 = FixedI64::from_u32(4);
410	b.neg().add(b.mul(b).sub(FOUR.mul(a).mul(c)).sqrt()).div(TWO.mul(a))
411}
412
413impl Curve {
414	/// Create a `Curve::Linear` instance from a high-level description.
415	///
416	/// WARNING: This is a `const` function designed for convenient use at build time and
417	/// will panic on overflow. Ensure that any inputs are sensible.
418	pub const fn make_linear(length: u128, period: u128, floor: FixedI64, ceil: FixedI64) -> Curve {
419		let length = FixedI64::from_rational(length, period).into_perbill();
420		let floor = floor.into_perbill();
421		let ceil = ceil.into_perbill();
422		Curve::LinearDecreasing { length, floor, ceil }
423	}
424
425	/// Create a `Curve::Reciprocal` instance from a high-level description.
426	///
427	/// WARNING: This is a `const` function designed for convenient use at build time and
428	/// will panic on overflow. Ensure that any inputs are sensible.
429	pub const fn make_reciprocal(
430		delay: u128,
431		period: u128,
432		level: FixedI64,
433		floor: FixedI64,
434		ceil: FixedI64,
435	) -> Curve {
436		let delay = FixedI64::from_rational(delay, period).into_perbill();
437		let mut bounds = (
438			(
439				FixedI64::from_u32(0),
440				Self::reciprocal_from_parts(FixedI64::from_u32(0), floor, ceil),
441				FixedI64::from_inner(i64::max_value()),
442			),
443			(
444				FixedI64::from_u32(1),
445				Self::reciprocal_from_parts(FixedI64::from_u32(1), floor, ceil),
446				FixedI64::from_inner(i64::max_value()),
447			),
448		);
449		const TWO: FixedI64 = FixedI64::from_u32(2);
450		while (bounds.1).0.sub((bounds.0).0).into_inner() > 1 {
451			let factor = (bounds.0).0.add((bounds.1).0).div(TWO);
452			let curve = Self::reciprocal_from_parts(factor, floor, ceil);
453			let curve_level = FixedI64::from_perbill(curve.const_threshold(delay));
454			if curve_level.into_inner() > level.into_inner() {
455				bounds = (bounds.0, (factor, curve, curve_level.sub(level)));
456			} else {
457				bounds = ((factor, curve, level.sub(curve_level)), bounds.1);
458			}
459		}
460		if (bounds.0).2.into_inner() < (bounds.1).2.into_inner() {
461			(bounds.0).1
462		} else {
463			(bounds.1).1
464		}
465	}
466
467	/// Create a `Curve::Reciprocal` instance from basic parameters.
468	///
469	/// WARNING: This is a `const` function designed for convenient use at build time and
470	/// will panic on overflow. Ensure that any inputs are sensible.
471	const fn reciprocal_from_parts(factor: FixedI64, floor: FixedI64, ceil: FixedI64) -> Self {
472		let delta = ceil.sub(floor);
473		let x_offset = pos_quad_solution(delta, delta, factor.neg());
474		let y_offset = floor.sub(factor.div(FixedI64::from_u32(1).add(x_offset)));
475		Curve::Reciprocal { factor, x_offset, y_offset }
476	}
477
478	/// Print some info on the curve.
479	#[cfg(feature = "std")]
480	pub fn info(&self, days: u32, name: impl std::fmt::Display) {
481		let hours = days * 24;
482		println!("Curve {} := {:?}:", name, self);
483		println!("   t + 0h:   {:?}", self.threshold(Perbill::zero()));
484		println!("   t + 1h:   {:?}", self.threshold(Perbill::from_rational(1, hours)));
485		println!("   t + 2h:   {:?}", self.threshold(Perbill::from_rational(2, hours)));
486		println!("   t + 3h:   {:?}", self.threshold(Perbill::from_rational(3, hours)));
487		println!("   t + 6h:   {:?}", self.threshold(Perbill::from_rational(6, hours)));
488		println!("   t + 12h:  {:?}", self.threshold(Perbill::from_rational(12, hours)));
489		println!("   t + 24h:  {:?}", self.threshold(Perbill::from_rational(24, hours)));
490		let mut l = 0;
491		for &(n, d) in [(1, 12), (1, 8), (1, 4), (1, 2), (3, 4), (1, 1)].iter() {
492			let t = days * n / d;
493			if t != l {
494				println!("   t + {}d:   {:?}", t, self.threshold(Perbill::from_rational(t, days)));
495				l = t;
496			}
497		}
498		let t = |p: Perbill| -> std::string::String {
499			if p.is_one() {
500				"never".into()
501			} else {
502				let minutes = p * (hours * 60);
503				if minutes < 60 {
504					format!("{} minutes", minutes)
505				} else if minutes < 8 * 60 && !minutes.is_multiple_of(60) {
506					format!("{} hours {} minutes", minutes / 60, minutes % 60)
507				} else if minutes < 72 * 60 {
508					format!("{} hours", minutes / 60)
509				} else if (minutes / 60).is_multiple_of(24) {
510					format!("{} days", minutes / 60 / 24)
511				} else {
512					format!("{} days {} hours", minutes / 60 / 24, minutes / 60 % 24)
513				}
514			}
515		};
516		if self.delay(Perbill::from_percent(49)) < Perbill::one() {
517			println!("   30% threshold:   {}", t(self.delay(Perbill::from_percent(30))));
518			println!("   10% threshold:   {}", t(self.delay(Perbill::from_percent(10))));
519			println!("   3% threshold:    {}", t(self.delay(Perbill::from_percent(3))));
520			println!("   1% threshold:    {}", t(self.delay(Perbill::from_percent(1))));
521			println!("   0.1% threshold:  {}", t(self.delay(Perbill::from_rational(1u32, 1_000))));
522			println!("   0.01% threshold: {}", t(self.delay(Perbill::from_rational(1u32, 10_000))));
523		} else {
524			println!(
525				"   99.9% threshold: {}",
526				t(self.delay(Perbill::from_rational(999u32, 1_000)))
527			);
528			println!("   99% threshold:   {}", t(self.delay(Perbill::from_percent(99))));
529			println!("   95% threshold:   {}", t(self.delay(Perbill::from_percent(95))));
530			println!("   90% threshold:   {}", t(self.delay(Perbill::from_percent(90))));
531			println!("   75% threshold:   {}", t(self.delay(Perbill::from_percent(75))));
532			println!("   60% threshold:   {}", t(self.delay(Perbill::from_percent(60))));
533		}
534	}
535
536	/// Determine the `y` value for the given `x` value.
537	pub fn threshold(&self, x: Perbill) -> Perbill {
538		match self {
539			Self::LinearDecreasing { length, floor, ceil } => {
540				*ceil - (x.min(*length).saturating_div(*length, Down) * (*ceil - *floor))
541			},
542			Self::SteppedDecreasing { begin, end, step, period } => {
543				(*begin - (step.int_mul(x.int_div(*period))).min(*begin)).max(*end)
544			},
545			Self::Reciprocal { factor, x_offset, y_offset } => factor
546				.checked_rounding_div(FixedI64::from(x) + *x_offset, Low)
547				.map(|yp| (yp + *y_offset).into_clamped_perthing())
548				.unwrap_or_else(Perbill::one),
549		}
550	}
551
552	/// Determine the `y` value for the given `x` value.
553	///
554	/// This is a partial implementation designed only for use in const functions.
555	const fn const_threshold(&self, x: Perbill) -> Perbill {
556		match self {
557			Self::Reciprocal { factor, x_offset, y_offset } => {
558				match factor.checked_rounding_div(FixedI64::from_perbill(x).add(*x_offset), Low) {
559					Some(yp) => (yp.add(*y_offset)).into_perbill(),
560					None => Perbill::one(),
561				}
562			},
563			_ => panic!("const_threshold cannot be used on this curve"),
564		}
565	}
566
567	/// Determine the smallest `x` value such that `passing` returns `true` when passed along with
568	/// the given `y` value.
569	///
570	/// If `passing` never returns `true` for any value of `x` when paired with `y`, then
571	/// `Perbill::one` may be returned.
572	///
573	/// ```nocompile
574	/// let c = Curve::LinearDecreasing { begin: Perbill::one(), delta: Perbill::one() };
575	/// //      ^^^ Can be any curve.
576	/// let y = Perbill::from_percent(50);
577	/// //      ^^^ Can be any value.
578	/// let x = c.delay(y);
579	/// assert!(c.passing(x, y));
580	/// ```
581	pub fn delay(&self, y: Perbill) -> Perbill {
582		match self {
583			Self::LinearDecreasing { length, floor, ceil } => {
584				if y < *floor {
585					Perbill::one()
586				} else if y > *ceil {
587					Perbill::zero()
588				} else {
589					(*ceil - y).saturating_div(*ceil - *floor, Up).saturating_mul(*length)
590				}
591			},
592			Self::SteppedDecreasing { begin, end, step, period } => {
593				if y < *end {
594					Perbill::one()
595				} else {
596					period.int_mul((*begin - y.min(*begin) + step.less_epsilon()).int_div(*step))
597				}
598			},
599			Self::Reciprocal { factor, x_offset, y_offset } => {
600				let y = FixedI64::from(y);
601				let maybe_term = factor.checked_rounding_div(y - *y_offset, High);
602				maybe_term
603					.and_then(|term| (term - *x_offset).try_into_perthing().ok())
604					.unwrap_or_else(Perbill::one)
605			},
606		}
607	}
608
609	/// Return `true` iff the `y` value is greater than the curve at the `x`.
610	pub fn passing(&self, x: Perbill, y: Perbill) -> bool {
611		y >= self.threshold(x)
612	}
613}
614
615#[cfg(feature = "std")]
616impl Debug for Curve {
617	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
618		match self {
619			Self::LinearDecreasing { length, floor, ceil } => {
620				write!(
621					f,
622					"Linear[(0%, {:?}) -> ({:?}, {:?}) -> (100%, {:?})]",
623					ceil, length, floor, floor,
624				)
625			},
626			Self::SteppedDecreasing { begin, end, step, period } => {
627				write!(
628					f,
629					"Stepped[(0%, {:?}) -> (100%, {:?}) by ({:?}, {:?})]",
630					begin, end, period, step,
631				)
632			},
633			Self::Reciprocal { factor, x_offset, y_offset } => {
634				write!(
635					f,
636					"Reciprocal[factor of {:?}, x_offset of {:?}, y_offset of {:?}]",
637					factor, x_offset, y_offset,
638				)
639			},
640		}
641	}
642}
643
644#[cfg(test)]
645mod tests {
646	use super::*;
647	use frame_support::traits::ConstU32;
648	use sp_runtime::{str_array as s, PerThing};
649
650	const fn percent(x: u128) -> FixedI64 {
651		FixedI64::from_rational(x, 100)
652	}
653
654	const TIP_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100));
655	const TIP_SUP: Curve = Curve::make_reciprocal(1, 28, percent(4), percent(0), percent(50));
656	const ROOT_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100));
657	const ROOT_SUP: Curve = Curve::make_linear(28, 28, percent(0), percent(50));
658	const WHITE_APP: Curve =
659		Curve::make_reciprocal(16, 28 * 24, percent(96), percent(50), percent(100));
660	const WHITE_SUP: Curve = Curve::make_reciprocal(1, 28, percent(20), percent(10), percent(50));
661	const SMALL_APP: Curve = Curve::make_linear(10, 28, percent(50), percent(100));
662	const SMALL_SUP: Curve = Curve::make_reciprocal(8, 28, percent(1), percent(0), percent(50));
663	const MID_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100));
664	const MID_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50));
665	const BIG_APP: Curve = Curve::make_linear(23, 28, percent(50), percent(100));
666	const BIG_SUP: Curve = Curve::make_reciprocal(16, 28, percent(1), percent(0), percent(50));
667	const HUGE_APP: Curve = Curve::make_linear(28, 28, percent(50), percent(100));
668	const HUGE_SUP: Curve = Curve::make_reciprocal(20, 28, percent(1), percent(0), percent(50));
669	const PARAM_APP: Curve = Curve::make_reciprocal(4, 28, percent(80), percent(50), percent(100));
670	const PARAM_SUP: Curve = Curve::make_reciprocal(7, 28, percent(10), percent(0), percent(50));
671	const ADMIN_APP: Curve = Curve::make_linear(17, 28, percent(50), percent(100));
672	const ADMIN_SUP: Curve = Curve::make_reciprocal(12, 28, percent(1), percent(0), percent(50));
673
674	// TODO: ceil for linear.
675
676	#[test]
677	#[should_panic]
678	fn check_curves() {
679		TIP_APP.info(28u32, "Tip Approval");
680		TIP_SUP.info(28u32, "Tip Support");
681		ROOT_APP.info(28u32, "Root Approval");
682		ROOT_SUP.info(28u32, "Root Support");
683		WHITE_APP.info(28u32, "Whitelist Approval");
684		WHITE_SUP.info(28u32, "Whitelist Support");
685		SMALL_APP.info(28u32, "Small Spend Approval");
686		SMALL_SUP.info(28u32, "Small Spend Support");
687		MID_APP.info(28u32, "Mid Spend Approval");
688		MID_SUP.info(28u32, "Mid Spend Support");
689		BIG_APP.info(28u32, "Big Spend Approval");
690		BIG_SUP.info(28u32, "Big Spend Support");
691		HUGE_APP.info(28u32, "Huge Spend Approval");
692		HUGE_SUP.info(28u32, "Huge Spend Support");
693		PARAM_APP.info(28u32, "Mid-tier Parameter Change Approval");
694		PARAM_SUP.info(28u32, "Mid-tier Parameter Change Support");
695		ADMIN_APP.info(28u32, "Admin (e.g. Cancel Slash) Approval");
696		ADMIN_SUP.info(28u32, "Admin (e.g. Cancel Slash) Support");
697		assert!(false);
698	}
699
700	#[test]
701	fn insert_sorted_works() {
702		let mut b: BoundedVec<u32, ConstU32<6>> = vec![20, 30, 40].try_into().unwrap();
703		assert!(b.insert_sorted_by_key(10, |&x| x));
704		assert_eq!(&b[..], &[10, 20, 30, 40][..]);
705
706		assert!(b.insert_sorted_by_key(60, |&x| x));
707		assert_eq!(&b[..], &[10, 20, 30, 40, 60][..]);
708
709		assert!(b.insert_sorted_by_key(50, |&x| x));
710		assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]);
711
712		assert!(!b.insert_sorted_by_key(9, |&x| x));
713		assert_eq!(&b[..], &[10, 20, 30, 40, 50, 60][..]);
714
715		assert!(b.insert_sorted_by_key(11, |&x| x));
716		assert_eq!(&b[..], &[11, 20, 30, 40, 50, 60][..]);
717
718		assert!(b.insert_sorted_by_key(21, |&x| x));
719		assert_eq!(&b[..], &[20, 21, 30, 40, 50, 60][..]);
720
721		assert!(b.insert_sorted_by_key(61, |&x| x));
722		assert_eq!(&b[..], &[21, 30, 40, 50, 60, 61][..]);
723
724		assert!(b.insert_sorted_by_key(51, |&x| x));
725		assert_eq!(&b[..], &[30, 40, 50, 51, 60, 61][..]);
726	}
727
728	#[test]
729	fn translated_reciprocal_works() {
730		let c: Curve = Curve::Reciprocal {
731			factor: FixedI64::from_float(0.03125),
732			x_offset: FixedI64::from_float(0.0363306838226),
733			y_offset: FixedI64::from_float(0.139845532427),
734		};
735		c.info(28u32, "Test");
736
737		for i in 0..9_696_969u32 {
738			let query = Perbill::from_rational(i, 9_696_969);
739			// Determine the nearest point in time when the query will be above threshold.
740			let delay_needed = c.delay(query);
741			// Ensure that it actually does pass at that time, or that it will never pass.
742			assert!(delay_needed.is_one() || c.passing(delay_needed, query));
743		}
744	}
745
746	#[test]
747	fn stepped_decreasing_works() {
748		fn pc(x: u32) -> Perbill {
749			Perbill::from_percent(x)
750		}
751
752		let c =
753			Curve::SteppedDecreasing { begin: pc(80), end: pc(30), step: pc(10), period: pc(15) };
754
755		for i in 0..9_696_969u32 {
756			let query = Perbill::from_rational(i, 9_696_969);
757			// Determine the nearest point in time when the query will be above threshold.
758			let delay_needed = c.delay(query);
759			// Ensure that it actually does pass at that time, or that it will never pass.
760			assert!(delay_needed.is_one() || c.passing(delay_needed, query));
761		}
762
763		assert_eq!(c.threshold(pc(0)), pc(80));
764		assert_eq!(c.threshold(pc(15).less_epsilon()), pc(80));
765		assert_eq!(c.threshold(pc(15)), pc(70));
766		assert_eq!(c.threshold(pc(30).less_epsilon()), pc(70));
767		assert_eq!(c.threshold(pc(30)), pc(60));
768		assert_eq!(c.threshold(pc(45).less_epsilon()), pc(60));
769		assert_eq!(c.threshold(pc(45)), pc(50));
770		assert_eq!(c.threshold(pc(60).less_epsilon()), pc(50));
771		assert_eq!(c.threshold(pc(60)), pc(40));
772		assert_eq!(c.threshold(pc(75).less_epsilon()), pc(40));
773		assert_eq!(c.threshold(pc(75)), pc(30));
774		assert_eq!(c.threshold(pc(100)), pc(30));
775
776		assert_eq!(c.delay(pc(100)), pc(0));
777		assert_eq!(c.delay(pc(80)), pc(0));
778		assert_eq!(c.delay(pc(80).less_epsilon()), pc(15));
779		assert_eq!(c.delay(pc(70)), pc(15));
780		assert_eq!(c.delay(pc(70).less_epsilon()), pc(30));
781		assert_eq!(c.delay(pc(60)), pc(30));
782		assert_eq!(c.delay(pc(60).less_epsilon()), pc(45));
783		assert_eq!(c.delay(pc(50)), pc(45));
784		assert_eq!(c.delay(pc(50).less_epsilon()), pc(60));
785		assert_eq!(c.delay(pc(40)), pc(60));
786		assert_eq!(c.delay(pc(40).less_epsilon()), pc(75));
787		assert_eq!(c.delay(pc(30)), pc(75));
788		assert_eq!(c.delay(pc(30).less_epsilon()), pc(100));
789		assert_eq!(c.delay(pc(0)), pc(100));
790	}
791
792	#[test]
793	fn tracks_integrity_check_detects_unsorted() {
794		use crate::mock::RuntimeOrigin;
795
796		pub struct BadTracksInfo;
797		impl TracksInfo<u64, u64> for BadTracksInfo {
798			type Id = u8;
799			type RuntimeOrigin = <RuntimeOrigin as OriginTrait>::PalletsOrigin;
800			fn tracks() -> impl Iterator<Item = Cow<'static, Track<Self::Id, u64, u64>>> {
801				static DATA: [Track<u8, u64, u64>; 2] = [
802					Track {
803						id: 1u8,
804						info: TrackInfo {
805							name: s("root"),
806							max_deciding: 1,
807							decision_deposit: 10,
808							prepare_period: 4,
809							decision_period: 4,
810							confirm_period: 2,
811							min_enactment_period: 4,
812							min_approval: Curve::LinearDecreasing {
813								length: Perbill::from_percent(100),
814								floor: Perbill::from_percent(50),
815								ceil: Perbill::from_percent(100),
816							},
817							min_support: Curve::LinearDecreasing {
818								length: Perbill::from_percent(100),
819								floor: Perbill::from_percent(0),
820								ceil: Perbill::from_percent(100),
821							},
822						},
823					},
824					Track {
825						id: 0u8,
826						info: TrackInfo {
827							name: s("none"),
828							max_deciding: 3,
829							decision_deposit: 1,
830							prepare_period: 2,
831							decision_period: 2,
832							confirm_period: 1,
833							min_enactment_period: 2,
834							min_approval: Curve::LinearDecreasing {
835								length: Perbill::from_percent(100),
836								floor: Perbill::from_percent(95),
837								ceil: Perbill::from_percent(100),
838							},
839							min_support: Curve::LinearDecreasing {
840								length: Perbill::from_percent(100),
841								floor: Perbill::from_percent(90),
842								ceil: Perbill::from_percent(100),
843							},
844						},
845					},
846				];
847				DATA.iter().map(Cow::Borrowed)
848			}
849			fn track_for(_: &Self::RuntimeOrigin) -> Result<Self::Id, ()> {
850				unimplemented!()
851			}
852		}
853
854		assert_eq!(
855			BadTracksInfo::check_integrity(),
856			Err("The tracks that were returned by `tracks` were not sorted by `Id`")
857		);
858	}
859
860	#[test]
861	fn encoding_and_decoding_of_string_like_structure_works() {
862		let string_like = StringLike::<13>(*b"hello, world!");
863		let encoded: Vec<u8> = string_like.encode();
864
865		let decoded_as_vec: Vec<u8> =
866			Decode::decode(&mut &encoded.clone()[..]).expect("decoding as Vec<u8> should work");
867		assert_eq!(decoded_as_vec.len(), 13);
868		let decoded_as_str: alloc::string::String =
869			Decode::decode(&mut &encoded.clone()[..]).expect("decoding as str should work");
870		assert_eq!(decoded_as_str.len(), 13);
871		let decoded_as_string_like: StringLike<13> =
872			Decode::decode(&mut &encoded.clone()[..]).expect("decoding as StringLike should work");
873		assert_eq!(decoded_as_string_like.0.len(), 13);
874	}
875}