pezpallet_lottery/
lib.rs

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