pallet_tips/
lib.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! # Tipping Pallet ( pallet-tips )
19//!
20//! > NOTE: This pallet is tightly coupled with pallet-treasury.
21//!
22//! A subsystem to allow for an agile "tipping" process, whereby a reward may be given without first
23//! having a pre-determined stakeholder group come to consensus on how much should be paid.
24//!
25//! A group of `Tippers` is determined through the config `Config`. After half of these have
26//! declared some amount that they believe a particular reported reason deserves, then a countdown
27//! period is entered where any remaining members can declare their tip amounts also. After the
28//! close of the countdown period, the median of all declared tips is paid to the reported
29//! beneficiary, along with any finders fee, in case of a public (and bonded) original report.
30//!
31//!
32//! ### Terminology
33//!
34//! Tipping protocol:
35//! - **Tipping:** The process of gathering declarations of amounts to tip and taking the median
36//!   amount to be transferred from the treasury to a beneficiary account.
37//! - **Tip Reason:** The reason for a tip; generally a URL which embodies or explains why a
38//!   particular individual (identified by an account ID) is worthy of a recognition by the
39//!   treasury.
40//! - **Finder:** The original public reporter of some reason for tipping.
41//! - **Finders Fee:** Some proportion of the tip amount that is paid to the reporter of the tip,
42//!   rather than the main beneficiary.
43//!
44//! ## Interface
45//!
46//! ### Dispatchable Functions
47//!
48//! Tipping protocol:
49//! - `report_awesome` - Report something worthy of a tip and register for a finders fee.
50//! - `retract_tip` - Retract a previous (finders fee registered) report.
51//! - `tip_new` - Report an item worthy of a tip and declare a specific amount to tip.
52//! - `tip` - Declare or redeclare an amount to tip for a particular reason.
53//! - `close_tip` - Close and pay out a tip.
54
55#![cfg_attr(not(feature = "std"), no_std)]
56
57mod benchmarking;
58mod tests;
59
60pub mod migrations;
61pub mod weights;
62
63extern crate alloc;
64
65use sp_runtime::{
66	traits::{AccountIdConversion, BadOrigin, Hash, StaticLookup, TrailingZeroInput, Zero},
67	Percent, RuntimeDebug,
68};
69
70use alloc::{vec, vec::Vec};
71use codec::{Decode, Encode};
72use frame_support::{
73	ensure,
74	traits::{
75		ContainsLengthBound, Currency, EnsureOrigin, ExistenceRequirement::KeepAlive, Get,
76		OnUnbalanced, ReservableCurrency, SortedMembers,
77	},
78	Parameter,
79};
80use frame_system::pallet_prelude::BlockNumberFor;
81
82#[cfg(any(feature = "try-runtime", test))]
83use sp_runtime::TryRuntimeError;
84
85pub use pallet::*;
86pub use weights::WeightInfo;
87
88const LOG_TARGET: &str = "runtime::tips";
89
90pub type BalanceOf<T, I = ()> = pallet_treasury::BalanceOf<T, I>;
91pub type NegativeImbalanceOf<T, I = ()> = pallet_treasury::NegativeImbalanceOf<T, I>;
92type AccountIdLookupOf<T> = <<T as frame_system::Config>::Lookup as StaticLookup>::Source;
93
94/// An open tipping "motion". Retains all details of a tip including information on the finder
95/// and the members who have voted.
96#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug, scale_info::TypeInfo)]
97pub struct OpenTip<
98	AccountId: Parameter,
99	Balance: Parameter,
100	BlockNumber: Parameter,
101	Hash: Parameter,
102> {
103	/// The hash of the reason for the tip. The reason should be a human-readable UTF-8 encoded
104	/// string. A URL would be sensible.
105	reason: Hash,
106	/// The account to be tipped.
107	who: AccountId,
108	/// The account who began this tip.
109	finder: AccountId,
110	/// The amount held on deposit for this tip.
111	deposit: Balance,
112	/// The block number at which this tip will close if `Some`. If `None`, then no closing is
113	/// scheduled.
114	closes: Option<BlockNumber>,
115	/// The members who have voted for this tip. Sorted by AccountId.
116	tips: Vec<(AccountId, Balance)>,
117	/// Whether this tip should result in the finder taking a fee.
118	finders_fee: bool,
119}
120
121#[frame_support::pallet]
122pub mod pallet {
123	use super::*;
124	use frame_support::pallet_prelude::*;
125	use frame_system::pallet_prelude::*;
126
127	/// The in-code storage version.
128	const STORAGE_VERSION: StorageVersion = StorageVersion::new(4);
129
130	#[pallet::pallet]
131	#[pallet::storage_version(STORAGE_VERSION)]
132	#[pallet::without_storage_info]
133	pub struct Pallet<T, I = ()>(_);
134
135	#[pallet::config]
136	pub trait Config<I: 'static = ()>: frame_system::Config + pallet_treasury::Config<I> {
137		/// The overarching event type.
138		type RuntimeEvent: From<Event<Self, I>>
139			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
140
141		/// Maximum acceptable reason length.
142		///
143		/// Benchmarks depend on this value, be sure to update weights file when changing this value
144		#[pallet::constant]
145		type MaximumReasonLength: Get<u32>;
146
147		/// The amount held on deposit per byte within the tip report reason or bounty description.
148		#[pallet::constant]
149		type DataDepositPerByte: Get<BalanceOf<Self, I>>;
150
151		/// The period for which a tip remains open after is has achieved threshold tippers.
152		#[pallet::constant]
153		type TipCountdown: Get<BlockNumberFor<Self>>;
154
155		/// The percent of the final tip which goes to the original reporter of the tip.
156		#[pallet::constant]
157		type TipFindersFee: Get<Percent>;
158
159		/// The non-zero amount held on deposit for placing a tip report.
160		#[pallet::constant]
161		type TipReportDepositBase: Get<BalanceOf<Self, I>>;
162
163		/// The maximum amount for a single tip.
164		#[pallet::constant]
165		type MaxTipAmount: Get<BalanceOf<Self, I>>;
166
167		/// Origin from which tippers must come.
168		///
169		/// `ContainsLengthBound::max_len` must be cost free (i.e. no storage read or heavy
170		/// operation). Benchmarks depend on the value of `ContainsLengthBound::max_len` be sure to
171		/// update weights file when altering this method.
172		type Tippers: SortedMembers<Self::AccountId> + ContainsLengthBound;
173
174		/// Handler for the unbalanced decrease when slashing for a removed tip.
175		type OnSlash: OnUnbalanced<NegativeImbalanceOf<Self, I>>;
176
177		/// Weight information for extrinsics in this pallet.
178		type WeightInfo: WeightInfo;
179	}
180
181	/// TipsMap that are not yet completed. Keyed by the hash of `(reason, who)` from the value.
182	/// This has the insecure enumerable hash function since the key itself is already
183	/// guaranteed to be a secure hash.
184	#[pallet::storage]
185	pub type Tips<T: Config<I>, I: 'static = ()> = StorageMap<
186		_,
187		Twox64Concat,
188		T::Hash,
189		OpenTip<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T>, T::Hash>,
190		OptionQuery,
191	>;
192
193	/// Simple preimage lookup from the reason's hash to the original data. Again, has an
194	/// insecure enumerable hash since the key is guaranteed to be the result of a secure hash.
195	#[pallet::storage]
196	pub type Reasons<T: Config<I>, I: 'static = ()> =
197		StorageMap<_, Identity, T::Hash, Vec<u8>, OptionQuery>;
198
199	#[pallet::event]
200	#[pallet::generate_deposit(pub(super) fn deposit_event)]
201	pub enum Event<T: Config<I>, I: 'static = ()> {
202		/// A new tip suggestion has been opened.
203		NewTip { tip_hash: T::Hash },
204		/// A tip suggestion has reached threshold and is closing.
205		TipClosing { tip_hash: T::Hash },
206		/// A tip suggestion has been closed.
207		TipClosed { tip_hash: T::Hash, who: T::AccountId, payout: BalanceOf<T, I> },
208		/// A tip suggestion has been retracted.
209		TipRetracted { tip_hash: T::Hash },
210		/// A tip suggestion has been slashed.
211		TipSlashed { tip_hash: T::Hash, finder: T::AccountId, deposit: BalanceOf<T, I> },
212	}
213
214	#[pallet::error]
215	pub enum Error<T, I = ()> {
216		/// The reason given is just too big.
217		ReasonTooBig,
218		/// The tip was already found/started.
219		AlreadyKnown,
220		/// The tip hash is unknown.
221		UnknownTip,
222		/// The tip given was too generous.
223		MaxTipAmountExceeded,
224		/// The account attempting to retract the tip is not the finder of the tip.
225		NotFinder,
226		/// The tip cannot be claimed/closed because there are not enough tippers yet.
227		StillOpen,
228		/// The tip cannot be claimed/closed because it's still in the countdown period.
229		Premature,
230	}
231
232	#[pallet::call]
233	impl<T: Config<I>, I: 'static> Pallet<T, I> {
234		/// Report something `reason` that deserves a tip and claim any eventual the finder's fee.
235		///
236		/// The dispatch origin for this call must be _Signed_.
237		///
238		/// Payment: `TipReportDepositBase` will be reserved from the origin account, as well as
239		/// `DataDepositPerByte` for each byte in `reason`.
240		///
241		/// - `reason`: The reason for, or the thing that deserves, the tip; generally this will be
242		///   a UTF-8-encoded URL.
243		/// - `who`: The account which should be credited for the tip.
244		///
245		/// Emits `NewTip` if successful.
246		///
247		/// ## Complexity
248		/// - `O(R)` where `R` length of `reason`.
249		///   - encoding and hashing of 'reason'
250		#[pallet::call_index(0)]
251		#[pallet::weight(<T as Config<I>>::WeightInfo::report_awesome(reason.len() as u32))]
252		pub fn report_awesome(
253			origin: OriginFor<T>,
254			reason: Vec<u8>,
255			who: AccountIdLookupOf<T>,
256		) -> DispatchResult {
257			let finder = ensure_signed(origin)?;
258			let who = T::Lookup::lookup(who)?;
259
260			ensure!(
261				reason.len() <= T::MaximumReasonLength::get() as usize,
262				Error::<T, I>::ReasonTooBig
263			);
264
265			let reason_hash = T::Hashing::hash(&reason[..]);
266			ensure!(!Reasons::<T, I>::contains_key(&reason_hash), Error::<T, I>::AlreadyKnown);
267			let hash = T::Hashing::hash_of(&(&reason_hash, &who));
268			ensure!(!Tips::<T, I>::contains_key(&hash), Error::<T, I>::AlreadyKnown);
269
270			let deposit = T::TipReportDepositBase::get() +
271				T::DataDepositPerByte::get() * (reason.len() as u32).into();
272			T::Currency::reserve(&finder, deposit)?;
273
274			Reasons::<T, I>::insert(&reason_hash, &reason);
275			let tip = OpenTip {
276				reason: reason_hash,
277				who,
278				finder,
279				deposit,
280				closes: None,
281				tips: vec![],
282				finders_fee: true,
283			};
284			Tips::<T, I>::insert(&hash, tip);
285			Self::deposit_event(Event::NewTip { tip_hash: hash });
286			Ok(())
287		}
288
289		/// Retract a prior tip-report from `report_awesome`, and cancel the process of tipping.
290		///
291		/// If successful, the original deposit will be unreserved.
292		///
293		/// The dispatch origin for this call must be _Signed_ and the tip identified by `hash`
294		/// must have been reported by the signing account through `report_awesome` (and not
295		/// through `tip_new`).
296		///
297		/// - `hash`: The identity of the open tip for which a tip value is declared. This is formed
298		///   as the hash of the tuple of the original tip `reason` and the beneficiary account ID.
299		///
300		/// Emits `TipRetracted` if successful.
301		///
302		/// ## Complexity
303		/// - `O(1)`
304		///   - Depends on the length of `T::Hash` which is fixed.
305		#[pallet::call_index(1)]
306		#[pallet::weight(<T as Config<I>>::WeightInfo::retract_tip())]
307		pub fn retract_tip(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
308			let who = ensure_signed(origin)?;
309			let tip = Tips::<T, I>::get(&hash).ok_or(Error::<T, I>::UnknownTip)?;
310			ensure!(tip.finder == who, Error::<T, I>::NotFinder);
311
312			Reasons::<T, I>::remove(&tip.reason);
313			Tips::<T, I>::remove(&hash);
314			if !tip.deposit.is_zero() {
315				let err_amount = T::Currency::unreserve(&who, tip.deposit);
316				debug_assert!(err_amount.is_zero());
317			}
318			Self::deposit_event(Event::TipRetracted { tip_hash: hash });
319			Ok(())
320		}
321
322		/// Give a tip for something new; no finder's fee will be taken.
323		///
324		/// The dispatch origin for this call must be _Signed_ and the signing account must be a
325		/// member of the `Tippers` set.
326		///
327		/// - `reason`: The reason for, or the thing that deserves, the tip; generally this will be
328		///   a UTF-8-encoded URL.
329		/// - `who`: The account which should be credited for the tip.
330		/// - `tip_value`: The amount of tip that the sender would like to give. The median tip
331		///   value of active tippers will be given to the `who`.
332		///
333		/// Emits `NewTip` if successful.
334		///
335		/// ## Complexity
336		/// - `O(R + T)` where `R` length of `reason`, `T` is the number of tippers.
337		///   - `O(T)`: decoding `Tipper` vec of length `T`. `T` is charged as upper bound given by
338		///     `ContainsLengthBound`. The actual cost depends on the implementation of
339		///     `T::Tippers`.
340		///   - `O(R)`: hashing and encoding of reason of length `R`
341		#[pallet::call_index(2)]
342		#[pallet::weight(<T as Config<I>>::WeightInfo::tip_new(reason.len() as u32, T::Tippers::max_len() as u32))]
343		pub fn tip_new(
344			origin: OriginFor<T>,
345			reason: Vec<u8>,
346			who: AccountIdLookupOf<T>,
347			#[pallet::compact] tip_value: BalanceOf<T, I>,
348		) -> DispatchResult {
349			let tipper = ensure_signed(origin)?;
350			let who = T::Lookup::lookup(who)?;
351			ensure!(T::Tippers::contains(&tipper), BadOrigin);
352
353			ensure!(T::MaxTipAmount::get() >= tip_value, Error::<T, I>::MaxTipAmountExceeded);
354
355			let reason_hash = T::Hashing::hash(&reason[..]);
356			ensure!(!Reasons::<T, I>::contains_key(&reason_hash), Error::<T, I>::AlreadyKnown);
357
358			let hash = T::Hashing::hash_of(&(&reason_hash, &who));
359			Reasons::<T, I>::insert(&reason_hash, &reason);
360			Self::deposit_event(Event::NewTip { tip_hash: hash });
361			let tips = vec![(tipper.clone(), tip_value)];
362			let tip = OpenTip {
363				reason: reason_hash,
364				who,
365				finder: tipper,
366				deposit: Zero::zero(),
367				closes: None,
368				tips,
369				finders_fee: false,
370			};
371			Tips::<T, I>::insert(&hash, tip);
372			Ok(())
373		}
374
375		/// Declare a tip value for an already-open tip.
376		///
377		/// The dispatch origin for this call must be _Signed_ and the signing account must be a
378		/// member of the `Tippers` set.
379		///
380		/// - `hash`: The identity of the open tip for which a tip value is declared. This is formed
381		///   as the hash of the tuple of the hash of the original tip `reason` and the beneficiary
382		///   account ID.
383		/// - `tip_value`: The amount of tip that the sender would like to give. The median tip
384		///   value of active tippers will be given to the `who`.
385		///
386		/// Emits `TipClosing` if the threshold of tippers has been reached and the countdown period
387		/// has started.
388		///
389		/// ## Complexity
390		/// - `O(T)` where `T` is the number of tippers. decoding `Tipper` vec of length `T`, insert
391		///   tip and check closing, `T` is charged as upper bound given by `ContainsLengthBound`.
392		///   The actual cost depends on the implementation of `T::Tippers`.
393		///
394		///   Actually weight could be lower as it depends on how many tips are in `OpenTip` but it
395		///   is weighted as if almost full i.e of length `T-1`.
396		#[pallet::call_index(3)]
397		#[pallet::weight(<T as Config<I>>::WeightInfo::tip(T::Tippers::max_len() as u32))]
398		pub fn tip(
399			origin: OriginFor<T>,
400			hash: T::Hash,
401			#[pallet::compact] tip_value: BalanceOf<T, I>,
402		) -> DispatchResult {
403			let tipper = ensure_signed(origin)?;
404			ensure!(T::Tippers::contains(&tipper), BadOrigin);
405
406			ensure!(T::MaxTipAmount::get() >= tip_value, Error::<T, I>::MaxTipAmountExceeded);
407
408			let mut tip = Tips::<T, I>::get(hash).ok_or(Error::<T, I>::UnknownTip)?;
409
410			if Self::insert_tip_and_check_closing(&mut tip, tipper, tip_value) {
411				Self::deposit_event(Event::TipClosing { tip_hash: hash });
412			}
413			Tips::<T, I>::insert(&hash, tip);
414			Ok(())
415		}
416
417		/// Close and payout a tip.
418		///
419		/// The dispatch origin for this call must be _Signed_.
420		///
421		/// The tip identified by `hash` must have finished its countdown period.
422		///
423		/// - `hash`: The identity of the open tip for which a tip value is declared. This is formed
424		///   as the hash of the tuple of the original tip `reason` and the beneficiary account ID.
425		///
426		/// ## Complexity
427		/// - : `O(T)` where `T` is the number of tippers. decoding `Tipper` vec of length `T`. `T`
428		///   is charged as upper bound given by `ContainsLengthBound`. The actual cost depends on
429		///   the implementation of `T::Tippers`.
430		#[pallet::call_index(4)]
431		#[pallet::weight(<T as Config<I>>::WeightInfo::close_tip(T::Tippers::max_len() as u32))]
432		pub fn close_tip(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
433			ensure_signed(origin)?;
434
435			let tip = Tips::<T, I>::get(hash).ok_or(Error::<T, I>::UnknownTip)?;
436			let n = tip.closes.as_ref().ok_or(Error::<T, I>::StillOpen)?;
437			ensure!(frame_system::Pallet::<T>::block_number() >= *n, Error::<T, I>::Premature);
438			// closed.
439			Reasons::<T, I>::remove(&tip.reason);
440			Tips::<T, I>::remove(hash);
441			Self::payout_tip(hash, tip);
442			Ok(())
443		}
444
445		/// Remove and slash an already-open tip.
446		///
447		/// May only be called from `T::RejectOrigin`.
448		///
449		/// As a result, the finder is slashed and the deposits are lost.
450		///
451		/// Emits `TipSlashed` if successful.
452		///
453		/// ## Complexity
454		/// - O(1).
455		#[pallet::call_index(5)]
456		#[pallet::weight(<T as Config<I>>::WeightInfo::slash_tip(T::Tippers::max_len() as u32))]
457		pub fn slash_tip(origin: OriginFor<T>, hash: T::Hash) -> DispatchResult {
458			T::RejectOrigin::ensure_origin(origin)?;
459
460			let tip = Tips::<T, I>::take(hash).ok_or(Error::<T, I>::UnknownTip)?;
461
462			if !tip.deposit.is_zero() {
463				let imbalance = T::Currency::slash_reserved(&tip.finder, tip.deposit).0;
464				T::OnSlash::on_unbalanced(imbalance);
465			}
466			Reasons::<T, I>::remove(&tip.reason);
467			Self::deposit_event(Event::TipSlashed {
468				tip_hash: hash,
469				finder: tip.finder,
470				deposit: tip.deposit,
471			});
472			Ok(())
473		}
474	}
475
476	#[pallet::hooks]
477	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
478		fn integrity_test() {
479			assert!(
480				!T::TipReportDepositBase::get().is_zero(),
481				"`TipReportDepositBase` should not be zero",
482			);
483		}
484
485		#[cfg(feature = "try-runtime")]
486		fn try_state(_n: BlockNumberFor<T>) -> Result<(), TryRuntimeError> {
487			Self::do_try_state()
488		}
489	}
490}
491
492impl<T: Config<I>, I: 'static> Pallet<T, I> {
493	// Add public immutables and private mutables.
494
495	/// Access tips storage from outside
496	pub fn tips(
497		hash: T::Hash,
498	) -> Option<OpenTip<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T>, T::Hash>> {
499		Tips::<T, I>::get(hash)
500	}
501
502	/// Access reasons storage from outside
503	pub fn reasons(hash: T::Hash) -> Option<Vec<u8>> {
504		Reasons::<T, I>::get(hash)
505	}
506
507	/// The account ID of the treasury pot.
508	///
509	/// This actually does computation. If you need to keep using it, then make sure you cache the
510	/// value and only call this once.
511	pub fn account_id() -> T::AccountId {
512		T::PalletId::get().into_account_truncating()
513	}
514
515	/// Given a mutable reference to an `OpenTip`, insert the tip into it and check whether it
516	/// closes, if so, then deposit the relevant event and set closing accordingly.
517	///
518	/// `O(T)` and one storage access.
519	fn insert_tip_and_check_closing(
520		tip: &mut OpenTip<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T>, T::Hash>,
521		tipper: T::AccountId,
522		tip_value: BalanceOf<T, I>,
523	) -> bool {
524		match tip.tips.binary_search_by_key(&&tipper, |x| &x.0) {
525			Ok(pos) => tip.tips[pos] = (tipper, tip_value),
526			Err(pos) => tip.tips.insert(pos, (tipper, tip_value)),
527		}
528		Self::retain_active_tips(&mut tip.tips);
529		let threshold = (T::Tippers::count() + 1) / 2;
530		if tip.tips.len() >= threshold && tip.closes.is_none() {
531			tip.closes = Some(frame_system::Pallet::<T>::block_number() + T::TipCountdown::get());
532			true
533		} else {
534			false
535		}
536	}
537
538	/// Remove any non-members of `Tippers` from a `tips` vector. `O(T)`.
539	fn retain_active_tips(tips: &mut Vec<(T::AccountId, BalanceOf<T, I>)>) {
540		let members = T::Tippers::sorted_members();
541		let mut members_iter = members.iter();
542		let mut member = members_iter.next();
543		tips.retain(|(ref a, _)| loop {
544			match member {
545				None => break false,
546				Some(m) if m > a => break false,
547				Some(m) => {
548					member = members_iter.next();
549					if m < a {
550						continue
551					} else {
552						break true
553					}
554				},
555			}
556		});
557	}
558
559	/// Execute the payout of a tip.
560	///
561	/// Up to three balance operations.
562	/// Plus `O(T)` (`T` is Tippers length).
563	fn payout_tip(
564		hash: T::Hash,
565		tip: OpenTip<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T>, T::Hash>,
566	) {
567		let mut tips = tip.tips;
568		Self::retain_active_tips(&mut tips);
569		tips.sort_by_key(|i| i.1);
570
571		let treasury = Self::account_id();
572		let max_payout = pallet_treasury::Pallet::<T, I>::pot();
573
574		let mut payout = tips[tips.len() / 2].1.min(max_payout);
575		if !tip.deposit.is_zero() {
576			let err_amount = T::Currency::unreserve(&tip.finder, tip.deposit);
577			debug_assert!(err_amount.is_zero());
578		}
579
580		if tip.finders_fee && tip.finder != tip.who {
581			// pay out the finder's fee.
582			let finders_fee = T::TipFindersFee::get() * payout;
583			payout -= finders_fee;
584			// this should go through given we checked it's at most the free balance, but still
585			// we only make a best-effort.
586			let res = T::Currency::transfer(&treasury, &tip.finder, finders_fee, KeepAlive);
587			debug_assert!(res.is_ok());
588		}
589
590		// same as above: best-effort only.
591		let res = T::Currency::transfer(&treasury, &tip.who, payout, KeepAlive);
592		debug_assert!(res.is_ok());
593		Self::deposit_event(Event::TipClosed { tip_hash: hash, who: tip.who, payout });
594	}
595
596	pub fn migrate_retract_tip_for_tip_new(module: &[u8], item: &[u8]) {
597		/// An open tipping "motion". Retains all details of a tip including information on the
598		/// finder and the members who have voted.
599		#[derive(Clone, Eq, PartialEq, Encode, Decode, RuntimeDebug)]
600		pub struct OldOpenTip<
601			AccountId: Parameter,
602			Balance: Parameter,
603			BlockNumber: Parameter,
604			Hash: Parameter,
605		> {
606			/// The hash of the reason for the tip. The reason should be a human-readable UTF-8
607			/// encoded string. A URL would be sensible.
608			reason: Hash,
609			/// The account to be tipped.
610			who: AccountId,
611			/// The account who began this tip and the amount held on deposit.
612			finder: Option<(AccountId, Balance)>,
613			/// The block number at which this tip will close if `Some`. If `None`, then no closing
614			/// is scheduled.
615			closes: Option<BlockNumber>,
616			/// The members who have voted for this tip. Sorted by AccountId.
617			tips: Vec<(AccountId, Balance)>,
618		}
619
620		use frame_support::{migration::storage_key_iter, Twox64Concat};
621
622		let zero_account = T::AccountId::decode(&mut TrailingZeroInput::new(&[][..]))
623			.expect("infinite input; qed");
624
625		for (hash, old_tip) in storage_key_iter::<
626			T::Hash,
627			OldOpenTip<T::AccountId, BalanceOf<T, I>, BlockNumberFor<T>, T::Hash>,
628			Twox64Concat,
629		>(module, item)
630		.drain()
631		{
632			let (finder, deposit, finders_fee) = match old_tip.finder {
633				Some((finder, deposit)) => (finder, deposit, true),
634				None => (zero_account.clone(), Zero::zero(), false),
635			};
636			let new_tip = OpenTip {
637				reason: old_tip.reason,
638				who: old_tip.who,
639				finder,
640				deposit,
641				closes: old_tip.closes,
642				tips: old_tip.tips,
643				finders_fee,
644			};
645			Tips::<T, I>::insert(hash, new_tip)
646		}
647	}
648
649	/// Ensure the correctness of the state of this pallet.
650	///
651	/// This should be valid before and after each state transition of this pallet.
652	///
653	/// ## Invariants:
654	/// 1. The number of entries in `Tips` should be equal to `Reasons`.
655	/// 2. Reasons exists for each Tip[`OpenTip.reason`].
656	/// 3. If `OpenTip.finders_fee` is true, then OpenTip.deposit should be greater than zero.
657	#[cfg(any(feature = "try-runtime", test))]
658	pub fn do_try_state() -> Result<(), TryRuntimeError> {
659		let reasons = Reasons::<T, I>::iter_keys().collect::<Vec<_>>();
660		let tips = Tips::<T, I>::iter_keys().collect::<Vec<_>>();
661
662		ensure!(
663			reasons.len() == tips.len(),
664			TryRuntimeError::Other("Equal length of entries in `Tips` and `Reasons` Storage")
665		);
666
667		for tip in Tips::<T, I>::iter_keys() {
668			let open_tip = Tips::<T, I>::get(&tip).expect("All map keys are valid; qed");
669
670			if open_tip.finders_fee {
671				ensure!(
672					!open_tip.deposit.is_zero(),
673					TryRuntimeError::Other(
674						"Tips with `finders_fee` should have non-zero `deposit`."
675					)
676				)
677			}
678
679			ensure!(
680				reasons.contains(&open_tip.reason),
681				TryRuntimeError::Other("no reason for this tip")
682			);
683		}
684		Ok(())
685	}
686}