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