Skip to main content

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