1#![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
79type 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: Balance,
89 start: BlockNumber,
91 length: BlockNumber,
93 delay: BlockNumber,
96 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 #[pezpallet::config]
132 pub trait Config: pezframe_system::Config {
133 #[pezpallet::constant]
135 type PalletId: Get<PalletId>;
136
137 type RuntimeCall: Parameter
139 + Dispatchable<RuntimeOrigin = Self::RuntimeOrigin>
140 + GetDispatchInfo
141 + From<pezframe_system::Call<Self>>;
142
143 type Currency: ReservableCurrency<Self::AccountId>;
145
146 type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
148
149 #[allow(deprecated)]
151 type RuntimeEvent: From<Event<Self>>
152 + IsType<<Self as pezframe_system::Config>::RuntimeEvent>;
153
154 type ManagerOrigin: EnsureOrigin<Self::RuntimeOrigin>;
156
157 #[pezpallet::constant]
159 type MaxCalls: Get<u32>;
160
161 type ValidateCall: ValidateCall<Self>;
167
168 #[pezpallet::constant]
172 type MaxGenerateRandom: Get<u32>;
173
174 type WeightInfo: WeightInfo;
176 }
177
178 #[pezpallet::event]
179 #[pezpallet::generate_deposit(pub(super) fn deposit_event)]
180 pub enum Event<T: Config> {
181 LotteryStarted,
183 CallsUpdated,
185 Winner { winner: T::AccountId, lottery_balance: BalanceOf<T> },
187 TicketBought { who: T::AccountId, call_index: CallIndex },
189 }
190
191 #[pezpallet::error]
192 pub enum Error<T> {
193 NotConfigured,
195 InProgress,
197 AlreadyEnded,
199 InvalidCall,
201 AlreadyParticipating,
203 TooManyCalls,
205 EncodingFailed,
207 }
208
209 #[pezpallet::storage]
210 pub(crate) type LotteryIndex<T> = StorageValue<_, u32, ValueQuery>;
211
212 #[pezpallet::storage]
214 pub(crate) type Lottery<T: Config> =
215 StorageValue<_, LotteryConfig<BlockNumberFor<T>, BalanceOf<T>>>;
216
217 #[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 #[pezpallet::storage]
229 pub(crate) type TicketsCount<T> = StorageValue<_, u32, ValueQuery>;
230
231 #[pezpallet::storage]
236 pub(crate) type Tickets<T: Config> = StorageMap<_, Twox64Concat, u32, T::AccountId>;
237
238 #[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 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 LotteryIndex::<T>::mutate(|index| *index = index.saturating_add(1));
271 config.start = n;
273 return T::WeightInfo::on_initialize_repeat();
274 } else {
275 *lottery = None;
277 return T::WeightInfo::on_initialize_end();
278 }
279 }
283 }
284 T::DbWeight::get().reads(1)
285 })
286 }
287 }
288
289 #[pezpallet::call]
290 impl<T: Config> Pezpallet<T> {
291 #[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 #[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 #[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 *lottery = Some(LotteryConfig { price, start, length, delay, repeat });
369 LotteryIndex::<T>::put(new_index);
370 Ok(())
371 })?;
372 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 #[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 pub fn account_id() -> T::AccountId {
406 T::PalletId::get().into_account_truncating()
407 }
408
409 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 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 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 fn do_buy_ticket(caller: &T::AccountId, call: &<T as Config>::RuntimeCall) -> DispatchResult {
442 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 Participants::<T>::try_mutate(
455 &caller,
456 |(lottery_index, participating_calls)| -> DispatchResult {
457 let index = LotteryIndex::<T>::get();
458 if *lottery_index != index {
460 *participating_calls = Default::default();
461 *lottery_index = index;
462 } else {
463 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 T::Currency::transfer(caller, &Self::account_id(), config.price, KeepAlive)?;
472 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 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 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 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 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}