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