// This file is part of Bizinikiwi. // Copyright (C) Parity Technologies (UK) Ltd. and Dijital Kurdistan Tech Institute // SPDX-License-Identifier: Apache-2.0 // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //! A lottery pezpallet that uses participation in the network to purchase tickets. //! //! With this pezpallet, you can configure a lottery, which is a pot of money that //! users contribute to, and that is reallocated to a single user at the end of //! the lottery period. Just like a normal lottery system, to participate, you //! need to "buy a ticket", which is used to fund the pot. //! //! The unique feature of this lottery system is that tickets can only be //! purchased by making a "valid call" dispatched through this pezpallet. //! By configuring certain calls to be valid for the lottery, you can encourage //! users to make those calls on your network. An example of how this could be //! used is to set validator nominations as a valid lottery call. If the lottery //! is set to repeat every month, then users would be encouraged to re-nominate //! validators every month. A user can only purchase one ticket per valid call //! per lottery. //! //! This pezpallet can be configured to use dynamically set calls or statically set //! calls. Call validation happens through the `ValidateCall` implementation. //! This pezpallet provides one implementation of this using the `CallIndices` //! storage item. You can also make your own implementation at the runtime level //! which can contain much more complex logic, such as validation of the //! parameters, which this pezpallet alone cannot do. //! //! This pezpallet uses the modulus operator to pick a random winner. It is known //! that this might introduce a bias if the random number chosen in a range that //! is not perfectly divisible by the total number of participants. The //! `MaxGenerateRandom` configuration can help mitigate this by generating new //! numbers until we hit the limit or we find a "fair" number. This is best //! effort only. #![cfg_attr(not(feature = "std"), no_std)] mod benchmarking; #[cfg(test)] mod mock; #[cfg(test)] mod tests; pub mod weights; extern crate alloc; use alloc::{boxed::Box, vec::Vec}; use codec::{Decode, Encode}; use pezframe_support::{ dispatch::{DispatchResult, GetDispatchInfo}, ensure, pezpallet_prelude::MaxEncodedLen, storage::bounded_vec::BoundedVec, traits::{Currency, ExistenceRequirement::KeepAlive, Get, Randomness, ReservableCurrency}, PalletId, }; pub use pezpallet::*; use pezsp_runtime::{ traits::{AccountIdConversion, Dispatchable, Saturating, Zero}, ArithmeticError, DispatchError, RuntimeDebug, }; pub use weights::WeightInfo; type BalanceOf = <::Currency as Currency<::AccountId>>::Balance; // Any runtime call can be encoded into two bytes which represent the pezpallet and call index. // We use this to uniquely match someone's incoming call with the calls configured for the lottery. type CallIndex = (u8, u8); #[derive( Encode, Decode, Default, Eq, PartialEq, RuntimeDebug, scale_info::TypeInfo, MaxEncodedLen, )] pub struct LotteryConfig { /// Price per entry. price: Balance, /// Starting block of the lottery. start: BlockNumber, /// Length of the lottery (start + length = end). length: BlockNumber, /// Delay for choosing the winner of the lottery. (start + length + delay = payout). /// Randomness in the "payout" block will be used to determine the winner. delay: BlockNumber, /// Whether this lottery will repeat after it completes. repeat: bool, } pub trait ValidateCall { fn validate_call(call: &::RuntimeCall) -> bool; } impl ValidateCall for () { fn validate_call(_: &::RuntimeCall) -> bool { false } } impl ValidateCall for Pezpallet { fn validate_call(call: &::RuntimeCall) -> bool { let valid_calls = CallIndices::::get(); let call_index = match Self::call_to_index(call) { Ok(call_index) => call_index, Err(_) => return false, }; valid_calls.iter().any(|c| call_index == *c) } } #[pezframe_support::pezpallet] pub mod pezpallet { use super::*; use pezframe_support::pezpallet_prelude::*; use pezframe_system::pezpallet_prelude::*; #[pezpallet::pezpallet] pub struct Pezpallet(_); /// The pezpallet's config trait. #[pezpallet::config] pub trait Config: pezframe_system::Config { /// The Lottery's pezpallet id #[pezpallet::constant] type PalletId: Get; /// A dispatchable call. type RuntimeCall: Parameter + Dispatchable + GetDispatchInfo + From>; /// The currency trait. type Currency: ReservableCurrency; /// Something that provides randomness in the runtime. type Randomness: Randomness>; /// The overarching event type. #[allow(deprecated)] type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The manager origin. type ManagerOrigin: EnsureOrigin; /// The max number of calls available in a single lottery. #[pezpallet::constant] type MaxCalls: Get; /// Used to determine if a call would be valid for purchasing a ticket. /// /// Be conscious of the implementation used here. We assume at worst that /// a vector of `MaxCalls` indices are queried for any call validation. /// You may need to provide a custom benchmark if this assumption is broken. type ValidateCall: ValidateCall; /// Number of time we should try to generate a random number that has no modulo bias. /// The larger this number, the more potential computation is used for picking the winner, /// but also the more likely that the chosen winner is done fairly. #[pezpallet::constant] type MaxGenerateRandom: Get; /// Weight information for extrinsics in this pezpallet. type WeightInfo: WeightInfo; } #[pezpallet::event] #[pezpallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// A lottery has been started! LotteryStarted, /// A new set of calls have been set! CallsUpdated, /// A winner has been chosen! Winner { winner: T::AccountId, lottery_balance: BalanceOf }, /// A ticket has been bought! TicketBought { who: T::AccountId, call_index: CallIndex }, } #[pezpallet::error] pub enum Error { /// A lottery has not been configured. NotConfigured, /// A lottery is already in progress. InProgress, /// A lottery has already ended. AlreadyEnded, /// The call is not valid for an open lottery. InvalidCall, /// You are already participating in the lottery with this call. AlreadyParticipating, /// Too many calls for a single lottery. TooManyCalls, /// Failed to encode calls EncodingFailed, } #[pezpallet::storage] pub(crate) type LotteryIndex = StorageValue<_, u32, ValueQuery>; /// The configuration for the current lottery. #[pezpallet::storage] pub(crate) type Lottery = StorageValue<_, LotteryConfig, BalanceOf>>; /// Users who have purchased a ticket. (Lottery Index, Tickets Purchased) #[pezpallet::storage] pub(crate) type Participants = StorageMap< _, Twox64Concat, T::AccountId, (u32, BoundedVec), ValueQuery, >; /// Total number of tickets sold. #[pezpallet::storage] pub(crate) type TicketsCount = StorageValue<_, u32, ValueQuery>; /// Each ticket's owner. /// /// May have residual storage from previous lotteries. Use `TicketsCount` to see which ones /// are actually valid ticket mappings. #[pezpallet::storage] pub(crate) type Tickets = StorageMap<_, Twox64Concat, u32, T::AccountId>; /// The calls stored in this pezpallet to be used in an active lottery if configured /// by `Config::ValidateCall`. #[pezpallet::storage] pub(crate) type CallIndices = StorageValue<_, BoundedVec, ValueQuery>; #[pezpallet::hooks] impl Hooks> for Pezpallet { fn on_initialize(n: BlockNumberFor) -> Weight { Lottery::::mutate(|mut lottery| -> Weight { if let Some(config) = &mut lottery { let payout_block = config.start.saturating_add(config.length).saturating_add(config.delay); if payout_block <= n { let (lottery_account, lottery_balance) = Self::pot(); let winner = Self::choose_account().unwrap_or(lottery_account); // Not much we can do if this fails... let res = T::Currency::transfer( &Self::account_id(), &winner, lottery_balance, KeepAlive, ); debug_assert!(res.is_ok()); Self::deposit_event(Event::::Winner { winner, lottery_balance }); TicketsCount::::kill(); if config.repeat { // If lottery should repeat, increment index by 1. LotteryIndex::::mutate(|index| *index = index.saturating_add(1)); // Set a new start with the current block. config.start = n; return T::WeightInfo::on_initialize_repeat(); } else { // Else, kill the lottery storage. *lottery = None; return T::WeightInfo::on_initialize_end(); } // We choose not need to kill Participants and Tickets to avoid a large // number of writes at one time. Instead, data persists between lotteries, // but is not used if it is not relevant. } } T::DbWeight::get().reads(1) }) } } #[pezpallet::call] impl Pezpallet { /// Buy a ticket to enter the lottery. /// /// This extrinsic acts as a passthrough function for `call`. In all /// situations where `call` alone would succeed, this extrinsic should /// succeed. /// /// If `call` is successful, then we will attempt to purchase a ticket, /// which may fail silently. To detect success of a ticket purchase, you /// should listen for the `TicketBought` event. /// /// This extrinsic must be called by a signed origin. #[pezpallet::call_index(0)] #[pezpallet::weight( T::WeightInfo::buy_ticket() .saturating_add(call.get_dispatch_info().call_weight) )] pub fn buy_ticket( origin: OriginFor, call: Box<::RuntimeCall>, ) -> DispatchResult { let caller = ensure_signed(origin.clone())?; call.clone().dispatch(origin).map_err(|e| e.error)?; let _ = Self::do_buy_ticket(&caller, &call); Ok(()) } /// Set calls in storage which can be used to purchase a lottery ticket. /// /// This function only matters if you use the `ValidateCall` implementation /// provided by this pezpallet, which uses storage to determine the valid calls. /// /// This extrinsic must be called by the Manager origin. #[pezpallet::call_index(1)] #[pezpallet::weight(T::WeightInfo::set_calls(calls.len() as u32))] pub fn set_calls( origin: OriginFor, calls: Vec<::RuntimeCall>, ) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; ensure!(calls.len() <= T::MaxCalls::get() as usize, Error::::TooManyCalls); if calls.is_empty() { CallIndices::::kill(); } else { let indices = Self::calls_to_indices(&calls)?; CallIndices::::put(indices); } Self::deposit_event(Event::::CallsUpdated); Ok(()) } /// Start a lottery using the provided configuration. /// /// This extrinsic must be called by the `ManagerOrigin`. /// /// Parameters: /// /// * `price`: The cost of a single ticket. /// * `length`: How long the lottery should run for starting at the current block. /// * `delay`: How long after the lottery end we should wait before picking a winner. /// * `repeat`: If the lottery should repeat when completed. #[pezpallet::call_index(2)] #[pezpallet::weight(T::WeightInfo::start_lottery())] pub fn start_lottery( origin: OriginFor, price: BalanceOf, length: BlockNumberFor, delay: BlockNumberFor, repeat: bool, ) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; Lottery::::try_mutate(|lottery| -> DispatchResult { ensure!(lottery.is_none(), Error::::InProgress); let index = LotteryIndex::::get(); let new_index = index.checked_add(1).ok_or(ArithmeticError::Overflow)?; let start = pezframe_system::Pezpallet::::block_number(); // Use new_index to more easily track everything with the current state. *lottery = Some(LotteryConfig { price, start, length, delay, repeat }); LotteryIndex::::put(new_index); Ok(()) })?; // Make sure pot exists. let lottery_account = Self::account_id(); if T::Currency::total_balance(&lottery_account).is_zero() { let _ = T::Currency::deposit_creating(&lottery_account, T::Currency::minimum_balance()); } Self::deposit_event(Event::::LotteryStarted); Ok(()) } /// If a lottery is repeating, you can use this to stop the repeat. /// The lottery will continue to run to completion. /// /// This extrinsic must be called by the `ManagerOrigin`. #[pezpallet::call_index(3)] #[pezpallet::weight(T::WeightInfo::stop_repeat())] pub fn stop_repeat(origin: OriginFor) -> DispatchResult { T::ManagerOrigin::ensure_origin(origin)?; Lottery::::mutate(|mut lottery| { if let Some(config) = &mut lottery { config.repeat = false } }); Ok(()) } } } impl Pezpallet { /// The account ID of the lottery pot. /// /// This actually does computation. If you need to keep using it, then make sure you cache the /// value and only call this once. pub fn account_id() -> T::AccountId { T::PalletId::get().into_account_truncating() } /// Return the pot account and amount of money in the pot. /// The existential deposit is not part of the pot so lottery account never gets deleted. fn pot() -> (T::AccountId, BalanceOf) { let account_id = Self::account_id(); let balance = T::Currency::free_balance(&account_id).saturating_sub(T::Currency::minimum_balance()); (account_id, balance) } /// Converts a vector of calls into a vector of call indices. fn calls_to_indices( calls: &[::RuntimeCall], ) -> Result, DispatchError> { let mut indices = BoundedVec::::with_bounded_capacity(calls.len()); for c in calls.iter() { let index = Self::call_to_index(c)?; indices.try_push(index).map_err(|_| Error::::TooManyCalls)?; } Ok(indices) } /// Convert a call to it's call index by encoding the call and taking the first two bytes. fn call_to_index(call: &::RuntimeCall) -> Result { let encoded_call = call.encode(); if encoded_call.len() < 2 { return Err(Error::::EncodingFailed.into()); } Ok((encoded_call[0], encoded_call[1])) } /// Logic for buying a ticket. fn do_buy_ticket(caller: &T::AccountId, call: &::RuntimeCall) -> DispatchResult { // Check the call is valid lottery let config = Lottery::::get().ok_or(Error::::NotConfigured)?; let block_number = pezframe_system::Pezpallet::::block_number(); ensure!( block_number < config.start.saturating_add(config.length), Error::::AlreadyEnded ); ensure!(T::ValidateCall::validate_call(call), Error::::InvalidCall); let call_index = Self::call_to_index(call)?; let ticket_count = TicketsCount::::get(); let new_ticket_count = ticket_count.checked_add(1).ok_or(ArithmeticError::Overflow)?; // Try to update the participant status Participants::::try_mutate( &caller, |(lottery_index, participating_calls)| -> DispatchResult { let index = LotteryIndex::::get(); // If lottery index doesn't match, then reset participating calls and index. if *lottery_index != index { *participating_calls = Default::default(); *lottery_index = index; } else { // Check that user is not already participating under this call. ensure!( !participating_calls.iter().any(|c| call_index == *c), Error::::AlreadyParticipating ); } participating_calls.try_push(call_index).map_err(|_| Error::::TooManyCalls)?; // Check user has enough funds and send it to the Lottery account. T::Currency::transfer(caller, &Self::account_id(), config.price, KeepAlive)?; // Create a new ticket. TicketsCount::::put(new_ticket_count); Tickets::::insert(ticket_count, caller.clone()); Ok(()) }, )?; Self::deposit_event(Event::::TicketBought { who: caller.clone(), call_index }); Ok(()) } /// Randomly choose a winning ticket and return the account that purchased it. /// The more tickets an account bought, the higher are its chances of winning. /// Returns `None` if there is no winner. fn choose_account() -> Option { match Self::choose_ticket(TicketsCount::::get()) { None => None, Some(ticket) => Tickets::::get(ticket), } } /// Randomly choose a winning ticket from among the total number of tickets. /// Returns `None` if there are no tickets. fn choose_ticket(total: u32) -> Option { if total == 0 { return None; } let mut random_number = Self::generate_random_number(0); // Best effort attempt to remove bias from modulus operator. for i in 1..T::MaxGenerateRandom::get() { if random_number < u32::MAX - u32::MAX % total { break; } random_number = Self::generate_random_number(i); } Some(random_number % total) } /// Generate a random number from a given seed. /// Note that there is potential bias introduced by using modulus operator. /// You should call this function with different seed values until the random /// number lies within `u32::MAX - u32::MAX % n`. /// TODO: deal with randomness freshness /// https://github.com/pezkuwichain/pezkuwi-sdk/issues/33 fn generate_random_number(seed: u32) -> u32 { let (random_seed, _) = T::Randomness::random(&(T::PalletId::get(), seed).encode()); let random_number = ::decode(&mut random_seed.as_ref()) .expect("secure hashes should always be bigger than u32; qed"); random_number } }