referrerpolicy=no-referrer-when-downgrade

pallet_lottery/
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//! A lottery pallet that uses participation in the network to purchase tickets.
19//!
20//! With this pallet, you can configure a lottery, which is a pot of money that
21//! users contribute to, and that is reallocated to a single user at the end of
22//! the lottery period. Just like a normal lottery system, to participate, you
23//! need to "buy a ticket", which is used to fund the pot.
24//!
25//! The unique feature of this lottery system is that tickets can only be
26//! purchased by making a "valid call" dispatched through this pallet.
27//! By configuring certain calls to be valid for the lottery, you can encourage
28//! users to make those calls on your network. An example of how this could be
29//! used is to set validator nominations as a valid lottery call. If the lottery
30//! is set to repeat every month, then users would be encouraged to re-nominate
31//! validators every month. A user can only purchase one ticket per valid call
32//! per lottery.
33//!
34//! This pallet can be configured to use dynamically set calls or statically set
35//! calls. Call validation happens through the `ValidateCall` implementation.
36//! This pallet provides one implementation of this using the `CallIndices`
37//! storage item. You can also make your own implementation at the runtime level
38//! which can contain much more complex logic, such as validation of the
39//! parameters, which this pallet alone cannot do.
40//!
41//! This pallet uses the modulus operator to pick a random winner. It is known
42//! that this might introduce a bias if the random number chosen in a range that
43//! is not perfectly divisible by the total number of participants. The
44//! `MaxGenerateRandom` configuration can help mitigate this by generating new
45//! numbers until we hit the limit or we find a "fair" number. This is best
46//! effort only.
47
48#![cfg_attr(not(feature = "std"), no_std)]
49
50mod benchmarking;
51#[cfg(test)]
52mod mock;
53#[cfg(test)]
54mod tests;
55pub mod weights;
56
57extern crate alloc;
58
59use alloc::{boxed::Box, vec::Vec};
60use codec::{Decode, Encode};
61use frame_support::{
62	dispatch::{DispatchResult, GetDispatchInfo},
63	ensure,
64	pallet_prelude::MaxEncodedLen,
65	storage::bounded_vec::BoundedVec,
66	traits::{Currency, ExistenceRequirement::KeepAlive, Get, Randomness, ReservableCurrency},
67	PalletId,
68};
69pub use pallet::*;
70use sp_runtime::{
71	traits::{AccountIdConversion, Dispatchable, Saturating, Zero},
72	ArithmeticError, DispatchError, RuntimeDebug,
73};
74pub use weights::WeightInfo;
75
76type BalanceOf<T> =
77	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
78
79// Any runtime call can be encoded into two bytes which represent the pallet and call index.
80// We use this to uniquely match someone's incoming call with the calls configured for the lottery.
81type CallIndex = (u8, u8);
82
83#[derive(
84	Encode, Decode, Default, Eq, PartialEq, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen,
85)]
86pub struct LotteryConfig<BlockNumber, Balance> {
87	/// Price per entry.
88	price: Balance,
89	/// Starting block of the lottery.
90	start: BlockNumber,
91	/// Length of the lottery (start + length = end).
92	length: BlockNumber,
93	/// Delay for choosing the winner of the lottery. (start + length + delay = payout).
94	/// Randomness in the "payout" block will be used to determine the winner.
95	delay: BlockNumber,
96	/// Whether this lottery will repeat after it completes.
97	repeat: bool,
98}
99
100pub trait ValidateCall<T: Config> {
101	fn validate_call(call: &<T as Config>::RuntimeCall) -> bool;
102}
103
104impl<T: Config> ValidateCall<T> for () {
105	fn validate_call(_: &<T as Config>::RuntimeCall) -> bool {
106		false
107	}
108}
109
110impl<T: Config> ValidateCall<T> for Pallet<T> {
111	fn validate_call(call: &<T as Config>::RuntimeCall) -> bool {
112		let valid_calls = CallIndices::<T>::get();
113		let call_index = match Self::call_to_index(call) {
114			Ok(call_index) => call_index,
115			Err(_) => return false,
116		};
117		valid_calls.iter().any(|c| call_index == *c)
118	}
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	#[pallet::pallet]
128	pub struct Pallet<T>(_);
129
130	/// The pallet's config trait.
131	#[pallet::config]
132	pub trait Config: frame_system::Config {
133		/// The Lottery's pallet id
134		#[pallet::constant]
135		type PalletId: Get<PalletId>;
136
137		/// A dispatchable call.
138		type RuntimeCall: Parameter
139			+ Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
140			+ GetDispatchInfo
141			+ From<frame_system::Call<Self>>;
142
143		/// The currency trait.
144		type Currency: ReservableCurrency<Self::AccountId>;
145
146		/// Something that provides randomness in the runtime.
147		type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
148
149		/// The overarching event type.
150		#[allow(deprecated)]
151		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
152
153		/// The manager origin.
154		type ManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
155
156		/// The max number of calls available in a single lottery.
157		#[pallet::constant]
158		type MaxCalls: Get<u32>;
159
160		/// Used to determine if a call would be valid for purchasing a ticket.
161		///
162		/// Be conscious of the implementation used here. We assume at worst that
163		/// a vector of `MaxCalls` indices are queried for any call validation.
164		/// You may need to provide a custom benchmark if this assumption is broken.
165		type ValidateCall: ValidateCall<Self>;
166
167		/// Number of time we should try to generate a random number that has no modulo bias.
168		/// The larger this number, the more potential computation is used for picking the winner,
169		/// but also the more likely that the chosen winner is done fairly.
170		#[pallet::constant]
171		type MaxGenerateRandom: Get<u32>;
172
173		/// Weight information for extrinsics in this pallet.
174		type WeightInfo: WeightInfo;
175	}
176
177	#[pallet::event]
178	#[pallet::generate_deposit(pub(super) fn deposit_event)]
179	pub enum Event<T: Config> {
180		/// A lottery has been started!
181		LotteryStarted,
182		/// A new set of calls have been set!
183		CallsUpdated,
184		/// A winner has been chosen!
185		Winner { winner: T::AccountId, lottery_balance: BalanceOf<T> },
186		/// A ticket has been bought!
187		TicketBought { who: T::AccountId, call_index: CallIndex },
188	}
189
190	#[pallet::error]
191	pub enum Error<T> {
192		/// A lottery has not been configured.
193		NotConfigured,
194		/// A lottery is already in progress.
195		InProgress,
196		/// A lottery has already ended.
197		AlreadyEnded,
198		/// The call is not valid for an open lottery.
199		InvalidCall,
200		/// You are already participating in the lottery with this call.
201		AlreadyParticipating,
202		/// Too many calls for a single lottery.
203		TooManyCalls,
204		/// Failed to encode calls
205		EncodingFailed,
206	}
207
208	#[pallet::storage]
209	pub(crate) type LotteryIndex<T> = StorageValue<_, u32, ValueQuery>;
210
211	/// The configuration for the current lottery.
212	#[pallet::storage]
213	pub(crate) type Lottery<T: Config> =
214		StorageValue<_, LotteryConfig<BlockNumberFor<T>, BalanceOf<T>>>;
215
216	/// Users who have purchased a ticket. (Lottery Index, Tickets Purchased)
217	#[pallet::storage]
218	pub(crate) type Participants<T: Config> = StorageMap<
219		_,
220		Twox64Concat,
221		T::AccountId,
222		(u32, BoundedVec<CallIndex, T::MaxCalls>),
223		ValueQuery,
224	>;
225
226	/// Total number of tickets sold.
227	#[pallet::storage]
228	pub(crate) type TicketsCount<T> = StorageValue<_, u32, ValueQuery>;
229
230	/// Each ticket's owner.
231	///
232	/// May have residual storage from previous lotteries. Use `TicketsCount` to see which ones
233	/// are actually valid ticket mappings.
234	#[pallet::storage]
235	pub(crate) type Tickets<T: Config> = StorageMap<_, Twox64Concat, u32, T::AccountId>;
236
237	/// The calls stored in this pallet to be used in an active lottery if configured
238	/// by `Config::ValidateCall`.
239	#[pallet::storage]
240	pub(crate) type CallIndices<T: Config> =
241		StorageValue<_, BoundedVec<CallIndex, T::MaxCalls>, ValueQuery>;
242
243	#[pallet::hooks]
244	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
245		fn on_initialize(n: BlockNumberFor<T>) -> Weight {
246			Lottery::<T>::mutate(|mut lottery| -> Weight {
247				if let Some(config) = &mut lottery {
248					let payout_block =
249						config.start.saturating_add(config.length).saturating_add(config.delay);
250					if payout_block <= n {
251						let (lottery_account, lottery_balance) = Self::pot();
252
253						let winner = Self::choose_account().unwrap_or(lottery_account);
254						// Not much we can do if this fails...
255						let res = T::Currency::transfer(
256							&Self::account_id(),
257							&winner,
258							lottery_balance,
259							KeepAlive,
260						);
261						debug_assert!(res.is_ok());
262
263						Self::deposit_event(Event::<T>::Winner { winner, lottery_balance });
264
265						TicketsCount::<T>::kill();
266
267						if config.repeat {
268							// If lottery should repeat, increment index by 1.
269							LotteryIndex::<T>::mutate(|index| *index = index.saturating_add(1));
270							// Set a new start with the current block.
271							config.start = n;
272							return T::WeightInfo::on_initialize_repeat()
273						} else {
274							// Else, kill the lottery storage.
275							*lottery = None;
276							return T::WeightInfo::on_initialize_end()
277						}
278						// We choose not need to kill Participants and Tickets to avoid a large
279						// number of writes at one time. Instead, data persists between lotteries,
280						// but is not used if it is not relevant.
281					}
282				}
283				T::DbWeight::get().reads(1)
284			})
285		}
286	}
287
288	#[pallet::call]
289	impl<T: Config> Pallet<T> {
290		/// Buy a ticket to enter the lottery.
291		///
292		/// This extrinsic acts as a passthrough function for `call`. In all
293		/// situations where `call` alone would succeed, this extrinsic should
294		/// succeed.
295		///
296		/// If `call` is successful, then we will attempt to purchase a ticket,
297		/// which may fail silently. To detect success of a ticket purchase, you
298		/// should listen for the `TicketBought` event.
299		///
300		/// This extrinsic must be called by a signed origin.
301		#[pallet::call_index(0)]
302		#[pallet::weight(
303			T::WeightInfo::buy_ticket()
304				.saturating_add(call.get_dispatch_info().call_weight)
305		)]
306		pub fn buy_ticket(
307			origin: OriginFor<T>,
308			call: Box<<T as Config>::RuntimeCall>,
309		) -> DispatchResult {
310			let caller = ensure_signed(origin.clone())?;
311			call.clone().dispatch(origin).map_err(|e| e.error)?;
312
313			let _ = Self::do_buy_ticket(&caller, &call);
314			Ok(())
315		}
316
317		/// Set calls in storage which can be used to purchase a lottery ticket.
318		///
319		/// This function only matters if you use the `ValidateCall` implementation
320		/// provided by this pallet, which uses storage to determine the valid calls.
321		///
322		/// This extrinsic must be called by the Manager origin.
323		#[pallet::call_index(1)]
324		#[pallet::weight(T::WeightInfo::set_calls(calls.len() as u32))]
325		pub fn set_calls(
326			origin: OriginFor<T>,
327			calls: Vec<<T as Config>::RuntimeCall>,
328		) -> DispatchResult {
329			T::ManagerOrigin::ensure_origin(origin)?;
330			ensure!(calls.len() <= T::MaxCalls::get() as usize, Error::<T>::TooManyCalls);
331			if calls.is_empty() {
332				CallIndices::<T>::kill();
333			} else {
334				let indices = Self::calls_to_indices(&calls)?;
335				CallIndices::<T>::put(indices);
336			}
337			Self::deposit_event(Event::<T>::CallsUpdated);
338			Ok(())
339		}
340
341		/// Start a lottery using the provided configuration.
342		///
343		/// This extrinsic must be called by the `ManagerOrigin`.
344		///
345		/// Parameters:
346		///
347		/// * `price`: The cost of a single ticket.
348		/// * `length`: How long the lottery should run for starting at the current block.
349		/// * `delay`: How long after the lottery end we should wait before picking a winner.
350		/// * `repeat`: If the lottery should repeat when completed.
351		#[pallet::call_index(2)]
352		#[pallet::weight(T::WeightInfo::start_lottery())]
353		pub fn start_lottery(
354			origin: OriginFor<T>,
355			price: BalanceOf<T>,
356			length: BlockNumberFor<T>,
357			delay: BlockNumberFor<T>,
358			repeat: bool,
359		) -> DispatchResult {
360			T::ManagerOrigin::ensure_origin(origin)?;
361			Lottery::<T>::try_mutate(|lottery| -> DispatchResult {
362				ensure!(lottery.is_none(), Error::<T>::InProgress);
363				let index = LotteryIndex::<T>::get();
364				let new_index = index.checked_add(1).ok_or(ArithmeticError::Overflow)?;
365				let start = frame_system::Pallet::<T>::block_number();
366				// Use new_index to more easily track everything with the current state.
367				*lottery = Some(LotteryConfig { price, start, length, delay, repeat });
368				LotteryIndex::<T>::put(new_index);
369				Ok(())
370			})?;
371			// Make sure pot exists.
372			let lottery_account = Self::account_id();
373			if T::Currency::total_balance(&lottery_account).is_zero() {
374				let _ =
375					T::Currency::deposit_creating(&lottery_account, T::Currency::minimum_balance());
376			}
377			Self::deposit_event(Event::<T>::LotteryStarted);
378			Ok(())
379		}
380
381		/// If a lottery is repeating, you can use this to stop the repeat.
382		/// The lottery will continue to run to completion.
383		///
384		/// This extrinsic must be called by the `ManagerOrigin`.
385		#[pallet::call_index(3)]
386		#[pallet::weight(T::WeightInfo::stop_repeat())]
387		pub fn stop_repeat(origin: OriginFor<T>) -> DispatchResult {
388			T::ManagerOrigin::ensure_origin(origin)?;
389			Lottery::<T>::mutate(|mut lottery| {
390				if let Some(config) = &mut lottery {
391					config.repeat = false
392				}
393			});
394			Ok(())
395		}
396	}
397}
398
399impl<T: Config> Pallet<T> {
400	/// The account ID of the lottery pot.
401	///
402	/// This actually does computation. If you need to keep using it, then make sure you cache the
403	/// value and only call this once.
404	pub fn account_id() -> T::AccountId {
405		T::PalletId::get().into_account_truncating()
406	}
407
408	/// Return the pot account and amount of money in the pot.
409	/// The existential deposit is not part of the pot so lottery account never gets deleted.
410	fn pot() -> (T::AccountId, BalanceOf<T>) {
411		let account_id = Self::account_id();
412		let balance =
413			T::Currency::free_balance(&account_id).saturating_sub(T::Currency::minimum_balance());
414
415		(account_id, balance)
416	}
417
418	/// Converts a vector of calls into a vector of call indices.
419	fn calls_to_indices(
420		calls: &[<T as Config>::RuntimeCall],
421	) -> Result<BoundedVec<CallIndex, T::MaxCalls>, DispatchError> {
422		let mut indices = BoundedVec::<CallIndex, T::MaxCalls>::with_bounded_capacity(calls.len());
423		for c in calls.iter() {
424			let index = Self::call_to_index(c)?;
425			indices.try_push(index).map_err(|_| Error::<T>::TooManyCalls)?;
426		}
427		Ok(indices)
428	}
429
430	/// Convert a call to it's call index by encoding the call and taking the first two bytes.
431	fn call_to_index(call: &<T as Config>::RuntimeCall) -> Result<CallIndex, DispatchError> {
432		let encoded_call = call.encode();
433		if encoded_call.len() < 2 {
434			return Err(Error::<T>::EncodingFailed.into())
435		}
436		Ok((encoded_call[0], encoded_call[1]))
437	}
438
439	/// Logic for buying a ticket.
440	fn do_buy_ticket(caller: &T::AccountId, call: &<T as Config>::RuntimeCall) -> DispatchResult {
441		// Check the call is valid lottery
442		let config = Lottery::<T>::get().ok_or(Error::<T>::NotConfigured)?;
443		let block_number = frame_system::Pallet::<T>::block_number();
444		ensure!(
445			block_number < config.start.saturating_add(config.length),
446			Error::<T>::AlreadyEnded
447		);
448		ensure!(T::ValidateCall::validate_call(call), Error::<T>::InvalidCall);
449		let call_index = Self::call_to_index(call)?;
450		let ticket_count = TicketsCount::<T>::get();
451		let new_ticket_count = ticket_count.checked_add(1).ok_or(ArithmeticError::Overflow)?;
452		// Try to update the participant status
453		Participants::<T>::try_mutate(
454			&caller,
455			|(lottery_index, participating_calls)| -> DispatchResult {
456				let index = LotteryIndex::<T>::get();
457				// If lottery index doesn't match, then reset participating calls and index.
458				if *lottery_index != index {
459					*participating_calls = Default::default();
460					*lottery_index = index;
461				} else {
462					// Check that user is not already participating under this call.
463					ensure!(
464						!participating_calls.iter().any(|c| call_index == *c),
465						Error::<T>::AlreadyParticipating
466					);
467				}
468				participating_calls.try_push(call_index).map_err(|_| Error::<T>::TooManyCalls)?;
469				// Check user has enough funds and send it to the Lottery account.
470				T::Currency::transfer(caller, &Self::account_id(), config.price, KeepAlive)?;
471				// Create a new ticket.
472				TicketsCount::<T>::put(new_ticket_count);
473				Tickets::<T>::insert(ticket_count, caller.clone());
474				Ok(())
475			},
476		)?;
477
478		Self::deposit_event(Event::<T>::TicketBought { who: caller.clone(), call_index });
479
480		Ok(())
481	}
482
483	/// Randomly choose a winning ticket and return the account that purchased it.
484	/// The more tickets an account bought, the higher are its chances of winning.
485	/// Returns `None` if there is no winner.
486	fn choose_account() -> Option<T::AccountId> {
487		match Self::choose_ticket(TicketsCount::<T>::get()) {
488			None => None,
489			Some(ticket) => Tickets::<T>::get(ticket),
490		}
491	}
492
493	/// Randomly choose a winning ticket from among the total number of tickets.
494	/// Returns `None` if there are no tickets.
495	fn choose_ticket(total: u32) -> Option<u32> {
496		if total == 0 {
497			return None
498		}
499		let mut random_number = Self::generate_random_number(0);
500
501		// Best effort attempt to remove bias from modulus operator.
502		for i in 1..T::MaxGenerateRandom::get() {
503			if random_number < u32::MAX - u32::MAX % total {
504				break
505			}
506
507			random_number = Self::generate_random_number(i);
508		}
509
510		Some(random_number % total)
511	}
512
513	/// Generate a random number from a given seed.
514	/// Note that there is potential bias introduced by using modulus operator.
515	/// You should call this function with different seed values until the random
516	/// number lies within `u32::MAX - u32::MAX % n`.
517	/// TODO: deal with randomness freshness
518	/// https://github.com/paritytech/substrate/issues/8311
519	fn generate_random_number(seed: u32) -> u32 {
520		let (random_seed, _) = T::Randomness::random(&(T::PalletId::get(), seed).encode());
521		let random_number = <u32>::decode(&mut random_seed.as_ref())
522			.expect("secure hashes should always be bigger than u32; qed");
523		random_number
524	}
525}