Participation Lottery Pallet (#7221)

* Basic design

* start adding tests

* finish tests

* clean up crates

* use call index for match

* finish benchmarks

* add to runtime

* fix

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_lottery --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/lottery/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* more efficient storage

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_lottery --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/lottery/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Update lib.rs

* Update bin/node/runtime/src/lib.rs

* trait -> config

* add repeating lottery

* new benchmarks

* fix build

* move trait for warning

* feedback from @xlc

* add stop_repeat

* fix

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_lottery --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/lottery/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* Support static calls

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_lottery --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/lottery/src/weights.rs --template=./.maintain/frame-weight-template.hbs

* fix test

* add loop to mitigate modulo bias

* Update weights for worst case scenario loop

* Initialize pot with ED

* cargo run --release --features=runtime-benchmarks --manifest-path=bin/node/cli/Cargo.toml -- benchmark --chain=dev --steps=50 --repeat=20 --pallet=pallet_lottery --extrinsic=* --execution=wasm --wasm-execution=compiled --heap-pages=4096 --output=./frame/lottery/src/weights.rs --template=./.maintain/frame-weight-template.hbs

Co-authored-by: Parity Benchmarking Bot <admin@parity.io>
This commit is contained in:
Shawn Tabrizi
2021-01-05 09:58:04 -04:00
committed by GitHub
parent d2eb87f5fc
commit 92f596829d
10 changed files with 1250 additions and 0 deletions
+42
View File
@@ -0,0 +1,42 @@
[package]
name = "pallet-lottery"
version = "2.0.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2018"
license = "Apache-2.0"
homepage = "https://substrate.dev"
repository = "https://github.com/paritytech/substrate/"
description = "FRAME Participation Lottery Pallet"
readme = "README.md"
[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]
[dependencies]
codec = { package = "parity-scale-codec", version = "1.3.4", default-features = false, features = ["derive"] }
sp-std = { version = "2.0.0", default-features = false, path = "../../primitives/std" }
sp-runtime = { version = "2.0.0", default-features = false, path = "../../primitives/runtime" }
frame-support = { version = "2.0.0", default-features = false, path = "../support" }
frame-system = { version = "2.0.0", default-features = false, path = "../system" }
frame-benchmarking = { version = "2.0.0", default-features = false, path = "../benchmarking", optional = true }
[dev-dependencies]
pallet-balances = { version = "2.0.0", path = "../balances" }
sp-core = { version = "2.0.0", path = "../../primitives/core" }
sp-io = { version = "2.0.0", path = "../../primitives/io" }
[features]
default = ["std"]
std = [
"codec/std",
"sp-std/std",
"frame-support/std",
"sp-runtime/std",
"frame-system/std",
]
runtime-benchmarks = [
"frame-benchmarking",
"frame-system/runtime-benchmarks",
"frame-support/runtime-benchmarks",
]
+192
View File
@@ -0,0 +1,192 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// 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.
//! Lottery pallet benchmarking.
#![cfg(feature = "runtime-benchmarks")]
use super::*;
use frame_system::RawOrigin;
use frame_support::traits::{OnInitialize, UnfilteredDispatchable};
use frame_benchmarking::{benchmarks, account, whitelisted_caller};
use sp_runtime::traits::{Bounded, Zero};
use crate::Module as Lottery;
// Set up and start a lottery
fn setup_lottery<T: Config>(repeat: bool) -> Result<(), &'static str> {
let price = T::Currency::minimum_balance();
let length = 10u32.into();
let delay = 5u32.into();
// Calls will be maximum length...
let mut calls = vec![
frame_system::Call::<T>::set_code(vec![]).into();
T::MaxCalls::get().saturating_sub(1)
];
// Last call will be the match for worst case scenario.
calls.push(frame_system::Call::<T>::remark(vec![]).into());
let origin = T::ManagerOrigin::successful_origin();
Lottery::<T>::set_calls(origin.clone(), calls)?;
Lottery::<T>::start_lottery(origin, price, length, delay, repeat)?;
Ok(())
}
benchmarks! {
_ { }
buy_ticket {
let caller = whitelisted_caller();
T::Currency::make_free_balance_be(&caller, BalanceOf::<T>::max_value());
setup_lottery::<T>(false)?;
// force user to have a long vec of calls participating
let set_code_index: CallIndex = Lottery::<T>::call_to_index(
&frame_system::Call::<T>::set_code(vec![]).into()
)?;
let already_called: (u32, Vec<CallIndex>) = (
LotteryIndex::get(),
vec![
set_code_index;
T::MaxCalls::get().saturating_sub(1)
],
);
Participants::<T>::insert(&caller, already_called);
let call = frame_system::Call::<T>::remark(vec![]);
}: _(RawOrigin::Signed(caller), Box::new(call.into()))
verify {
assert_eq!(TicketsCount::get(), 1);
}
set_calls {
let n in 0 .. T::MaxCalls::get() as u32;
let calls = vec![frame_system::Call::<T>::remark(vec![]).into(); n as usize];
let call = Call::<T>::set_calls(calls);
let origin = T::ManagerOrigin::successful_origin();
assert!(CallIndices::get().is_empty());
}: { call.dispatch_bypass_filter(origin)? }
verify {
if !n.is_zero() {
assert!(!CallIndices::get().is_empty());
}
}
start_lottery {
let price = BalanceOf::<T>::max_value();
let end = 10u32.into();
let payout = 5u32.into();
let call = Call::<T>::start_lottery(price, end, payout, true);
let origin = T::ManagerOrigin::successful_origin();
}: { call.dispatch_bypass_filter(origin)? }
verify {
assert!(crate::Lottery::<T>::get().is_some());
}
stop_repeat {
setup_lottery::<T>(true)?;
assert_eq!(crate::Lottery::<T>::get().unwrap().repeat, true);
let call = Call::<T>::stop_repeat();
let origin = T::ManagerOrigin::successful_origin();
}: { call.dispatch_bypass_filter(origin)? }
verify {
assert_eq!(crate::Lottery::<T>::get().unwrap().repeat, false);
}
on_initialize_end {
setup_lottery::<T>(false)?;
let winner = account("winner", 0, 0);
// User needs more than min balance to get ticket
T::Currency::make_free_balance_be(&winner, T::Currency::minimum_balance() * 10u32.into());
// Make sure lottery account has at least min balance too
let lottery_account = Lottery::<T>::account_id();
T::Currency::make_free_balance_be(&lottery_account, T::Currency::minimum_balance() * 10u32.into());
// Buy a ticket
let call = frame_system::Call::<T>::remark(vec![]);
Lottery::<T>::buy_ticket(RawOrigin::Signed(winner.clone()).into(), Box::new(call.into()))?;
// Kill user account for worst case
T::Currency::make_free_balance_be(&winner, 0u32.into());
// Assert that lotto is set up for winner
assert_eq!(TicketsCount::get(), 1);
assert!(!Lottery::<T>::pot().1.is_zero());
}: {
// Generate `MaxGenerateRandom` numbers for worst case scenario
for i in 0 .. T::MaxGenerateRandom::get() {
Lottery::<T>::generate_random_number(i);
}
// Start lottery has block 15 configured for payout
Lottery::<T>::on_initialize(15u32.into());
}
verify {
assert!(crate::Lottery::<T>::get().is_none());
assert_eq!(TicketsCount::get(), 0);
assert_eq!(Lottery::<T>::pot().1, 0u32.into());
assert!(!T::Currency::free_balance(&winner).is_zero())
}
on_initialize_repeat {
setup_lottery::<T>(true)?;
let winner = account("winner", 0, 0);
// User needs more than min balance to get ticket
T::Currency::make_free_balance_be(&winner, T::Currency::minimum_balance() * 10u32.into());
// Make sure lottery account has at least min balance too
let lottery_account = Lottery::<T>::account_id();
T::Currency::make_free_balance_be(&lottery_account, T::Currency::minimum_balance() * 10u32.into());
// Buy a ticket
let call = frame_system::Call::<T>::remark(vec![]);
Lottery::<T>::buy_ticket(RawOrigin::Signed(winner.clone()).into(), Box::new(call.into()))?;
// Kill user account for worst case
T::Currency::make_free_balance_be(&winner, 0u32.into());
// Assert that lotto is set up for winner
assert_eq!(TicketsCount::get(), 1);
assert!(!Lottery::<T>::pot().1.is_zero());
}: {
// Generate `MaxGenerateRandom` numbers for worst case scenario
for i in 0 .. T::MaxGenerateRandom::get() {
Lottery::<T>::generate_random_number(i);
}
// Start lottery has block 15 configured for payout
Lottery::<T>::on_initialize(15u32.into());
}
verify {
assert!(crate::Lottery::<T>::get().is_some());
assert_eq!(LotteryIndex::get(), 2);
assert_eq!(TicketsCount::get(), 0);
assert_eq!(Lottery::<T>::pot().1, 0u32.into());
assert!(!T::Currency::free_balance(&winner).is_zero())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mock::{new_test_ext, Test};
use frame_support::assert_ok;
#[test]
fn test_benchmarks() {
new_test_ext().execute_with(|| {
assert_ok!(test_benchmark_buy_ticket::<Test>());
assert_ok!(test_benchmark_set_calls::<Test>());
assert_ok!(test_benchmark_start_lottery::<Test>());
assert_ok!(test_benchmark_stop_repeat::<Test>());
assert_ok!(test_benchmark_on_initialize_end::<Test>());
assert_ok!(test_benchmark_on_initialize_repeat::<Test>());
});
}
}
+452
View File
@@ -0,0 +1,452 @@
// This file is part of Substrate.
// Copyright (C) 2017-2020 Parity Technologies (UK) Ltd.
// 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 pallet that uses participation in the network to purchase tickets.
//!
//! With this pallet, 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 pallet.
//! 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 ony purchase one ticket per valid call
//! per lottery.
//!
//! This pallet can be configured to use dynamically set calls or statically set
//! calls. Call validation happens through the `ValidateCall` implementation.
//! This pallet 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 pallet alone cannot do.
//!
//! This pallet 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)]
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
mod benchmarking;
pub mod weights;
use sp_std::prelude::*;
use sp_runtime::{
DispatchError, ModuleId,
traits::{AccountIdConversion, Saturating, Zero},
};
use frame_support::{
Parameter, decl_module, decl_error, decl_event, decl_storage, ensure, RuntimeDebug,
dispatch::{Dispatchable, DispatchResult, GetDispatchInfo},
traits::{
Currency, ReservableCurrency, Get, EnsureOrigin, ExistenceRequirement::KeepAlive, Randomness,
},
};
use frame_support::weights::Weight;
use frame_system::ensure_signed;
use codec::{Encode, Decode};
pub use weights::WeightInfo;
type BalanceOf<T> = <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
/// The module's config trait.
pub trait Config: frame_system::Config {
/// The Lottery's module id
type ModuleId: Get<ModuleId>;
/// A dispatchable call.
type Call: Parameter + Dispatchable<Origin=Self::Origin> + GetDispatchInfo + From<frame_system::Call<Self>>;
/// The currency trait.
type Currency: ReservableCurrency<Self::AccountId>;
/// Something that provides randomness in the runtime.
type Randomness: Randomness<Self::Hash>;
/// The overarching event type.
type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
/// The manager origin.
type ManagerOrigin: EnsureOrigin<Self::Origin>;
/// The max number of calls available in a single lottery.
type MaxCalls: Get<usize>;
/// 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<Self>;
/// 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.
type MaxGenerateRandom: Get<u32>;
/// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
// Any runtime call can be encoded into two bytes which represent the pallet 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)]
pub struct LotteryConfig<BlockNumber, Balance> {
/// 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<T: Config> {
fn validate_call(call: &<T as Config>::Call) -> bool;
}
impl<T: Config> ValidateCall<T> for () {
fn validate_call(_: &<T as Config>::Call) -> bool { false }
}
impl<T: Config> ValidateCall<T> for Module<T> {
fn validate_call(call: &<T as Config>::Call) -> 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)
}
}
decl_storage! {
trait Store for Module<T: Config> as Lottery {
LotteryIndex: u32;
/// The configuration for the current lottery.
Lottery: Option<LotteryConfig<T::BlockNumber, BalanceOf<T>>>;
/// Users who have purchased a ticket. (Lottery Index, Tickets Purchased)
Participants: map hasher(twox_64_concat) T::AccountId => (u32, Vec<CallIndex>);
/// Total number of tickets sold.
TicketsCount: u32;
/// Each ticket's owner.
///
/// May have residual storage from previous lotteries. Use `TicketsCount` to see which ones
/// are actually valid ticket mappings.
Tickets: map hasher(twox_64_concat) u32 => Option<T::AccountId>;
/// The calls stored in this pallet to be used in an active lottery if configured
/// by `Config::ValidateCall`.
CallIndices: Vec<CallIndex>;
}
}
decl_event!(
pub enum Event<T> where
<T as frame_system::Config>::AccountId,
Balance = BalanceOf<T>,
{
/// A lottery has been started!
LotteryStarted,
/// A new set of calls have been set!
CallsUpdated,
/// A winner has been chosen!
Winner(AccountId, Balance),
/// A ticket has been bought!
TicketBought(AccountId, CallIndex),
}
);
decl_error! {
pub enum Error for Module<T: Config> {
/// An overflow has occurred.
Overflow,
/// 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,
}
}
decl_module! {
pub struct Module<T: Config> for enum Call where origin: T::Origin, system = frame_system {
const ModuleId: ModuleId = T::ModuleId::get();
const MaxCalls: u32 = T::MaxCalls::get() as u32;
fn deposit_event() = default;
/// 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.
#[weight =
T::WeightInfo::buy_ticket()
.saturating_add(call.get_dispatch_info().weight)
]
fn buy_ticket(origin, call: Box<<T as Config>::Call>) {
let caller = ensure_signed(origin.clone())?;
call.clone().dispatch(origin).map_err(|e| e.error)?;
let _ = Self::do_buy_ticket(&caller, &call);
}
/// 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 pallet, which uses storage to determine the valid calls.
///
/// This extrinsic must be called by the Manager origin.
#[weight = T::WeightInfo::set_calls(calls.len() as u32)]
fn set_calls(origin, calls: Vec<<T as Config>::Call>) {
T::ManagerOrigin::ensure_origin(origin)?;
ensure!(calls.len() <= T::MaxCalls::get(), Error::<T>::TooManyCalls);
if calls.is_empty() {
CallIndices::kill();
} else {
let indices = Self::calls_to_indices(&calls)?;
CallIndices::put(indices);
}
Self::deposit_event(RawEvent::CallsUpdated);
}
/// 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.
#[weight = T::WeightInfo::start_lottery()]
fn start_lottery(origin,
price: BalanceOf<T>,
length: T::BlockNumber,
delay: T::BlockNumber,
repeat: bool,
) {
T::ManagerOrigin::ensure_origin(origin)?;
Lottery::<T>::try_mutate(|lottery| -> DispatchResult {
ensure!(lottery.is_none(), Error::<T>::InProgress);
let index = LotteryIndex::get();
let new_index = index.checked_add(1).ok_or(Error::<T>::Overflow)?;
let start = frame_system::Module::<T>::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() {
T::Currency::deposit_creating(&lottery_account, T::Currency::minimum_balance());
}
Self::deposit_event(RawEvent::LotteryStarted);
}
/// 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`.
#[weight = T::WeightInfo::stop_repeat()]
fn stop_repeat(origin) {
T::ManagerOrigin::ensure_origin(origin)?;
Lottery::<T>::mutate(|mut lottery| {
if let Some(config) = &mut lottery {
config.repeat = false
}
});
}
fn on_initialize(n: T::BlockNumber) -> Weight {
Lottery::<T>::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 ticket_count = TicketsCount::get();
let winning_number = Self::choose_winner(ticket_count);
let winner = Tickets::<T>::get(winning_number).unwrap_or(lottery_account);
// Not much we can do if this fails...
let _ = T::Currency::transfer(&Self::account_id(), &winner, lottery_balance, KeepAlive);
Self::deposit_event(RawEvent::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.
}
}
return T::DbWeight::get().reads(1)
})
}
}
}
impl<T: Config> Module<T> {
/// 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::ModuleId::get().into_account()
}
/// 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<T>) {
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: &[<T as Config>::Call]) -> Result<Vec<CallIndex>, DispatchError> {
let mut indices = Vec::with_capacity(calls.len());
for c in calls.iter() {
let index = Self::call_to_index(c)?;
indices.push(index)
}
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: &<T as Config>::Call) -> Result<CallIndex, DispatchError> {
let encoded_call = call.encode();
if encoded_call.len() < 2 { Err(Error::<T>::EncodingFailed)? }
return Ok((encoded_call[0], encoded_call[1]))
}
// Logic for buying a ticket.
fn do_buy_ticket(caller: &T::AccountId, call: &<T as Config>::Call) -> DispatchResult {
// Check the call is valid lottery
let config = Lottery::<T>::get().ok_or(Error::<T>::NotConfigured)?;
let block_number = frame_system::Module::<T>::block_number();
ensure!(block_number < config.start.saturating_add(config.length), Error::<T>::AlreadyEnded);
ensure!(T::ValidateCall::validate_call(call), Error::<T>::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(Error::<T>::Overflow)?;
// Try to update the participant status
Participants::<T>::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 = Vec::new();
*lottery_index = index;
} else {
// Check that user is not already participating under this call.
ensure!(!participating_calls.iter().any(|c| call_index == *c), Error::<T>::AlreadyParticipating);
}
// 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::<T>::insert(ticket_count, caller.clone());
participating_calls.push(call_index);
Ok(())
})?;
Self::deposit_event(RawEvent::TicketBought(caller.clone(), call_index));
Ok(())
}
// Randomly choose a winner from among the total number of participants.
fn choose_winner(total: u32) -> u32 {
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);
}
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`.
fn generate_random_number(seed: u32) -> u32 {
let random_seed = T::Randomness::random(&(T::ModuleId::get(), seed).encode());
let random_number = <u32>::decode(&mut random_seed.as_ref())
.expect("secure hashes should always be bigger than u32; qed");
random_number
}
}
+138
View File
@@ -0,0 +1,138 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// 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.
//! Test utilities
use super::*;
use frame_support::{
impl_outer_origin, impl_outer_dispatch, parameter_types,
traits::{OnInitialize, OnFinalize, TestRandomness},
};
use sp_core::H256;
use sp_runtime::{
Perbill,
testing::Header,
traits::{BlakeTwo256, IdentityLookup},
};
use frame_system::EnsureRoot;
impl_outer_origin! {
pub enum Origin for Test {}
}
impl_outer_dispatch! {
pub enum Call for Test where origin: Origin {
frame_system::System,
pallet_balances::Balances,
}
}
#[derive(Clone, Eq, PartialEq)]
pub struct Test;
parameter_types! {
pub const BlockHashCount: u64 = 250;
pub const MaximumBlockWeight: u32 = 1024;
pub const MaximumBlockLength: u32 = 2 * 1024;
pub const AvailableBlockRatio: Perbill = Perbill::one();
}
impl frame_system::Config for Test {
type BaseCallFilter = ();
type BlockWeights = ();
type BlockLength = ();
type DbWeight = ();
type Origin = Origin;
type Index = u64;
type Call = Call;
type BlockNumber = u64;
type Hash = H256;
type Hashing = BlakeTwo256;
type AccountId = u64;
type Lookup = IdentityLookup<Self::AccountId>;
type Header = Header;
type Event = ();
type BlockHashCount = BlockHashCount;
type Version = ();
type PalletInfo = ();
type AccountData = pallet_balances::AccountData<u64>;
type OnNewAccount = ();
type OnKilledAccount = ();
type SystemWeightInfo = ();
type SS58Prefix = ();
}
parameter_types! {
pub const ExistentialDeposit: u64 = 1;
}
impl pallet_balances::Config for Test {
type MaxLocks = ();
type Balance = u64;
type Event = ();
type DustRemoval = ();
type ExistentialDeposit = ExistentialDeposit;
type AccountStore = System;
type WeightInfo = ();
}
parameter_types! {
pub const LotteryModuleId: ModuleId = ModuleId(*b"py/lotto");
pub const MaxCalls: usize = 2;
pub const MaxGenerateRandom: u32 = 10;
}
impl Config for Test {
type ModuleId = LotteryModuleId;
type Call = Call;
type Currency = Balances;
type Randomness = TestRandomness;
type Event = ();
type ManagerOrigin = EnsureRoot<u64>;
type MaxCalls = MaxCalls;
type ValidateCall = Lottery;
type MaxGenerateRandom = MaxGenerateRandom;
type WeightInfo = ();
}
pub type Lottery = Module<Test>;
pub type System = frame_system::Module<Test>;
pub type Balances = pallet_balances::Module<Test>;
pub type SystemCall = frame_system::Call<Test>;
pub type BalancesCall = pallet_balances::Call<Test>;
pub fn new_test_ext() -> sp_io::TestExternalities {
let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
pallet_balances::GenesisConfig::<Test> {
balances: vec![(1, 100), (2, 100), (3, 100), (4, 100), (5, 100)],
}.assimilate_storage(&mut t).unwrap();
t.into()
}
/// Run until a particular block.
pub fn run_to_block(n: u64) {
while System::block_number() < n {
if System::block_number() > 1 {
Lottery::on_finalize(System::block_number());
System::on_finalize(System::block_number());
}
System::set_block_number(System::block_number() + 1);
System::on_initialize(System::block_number());
Lottery::on_initialize(System::block_number());
}
}
+261
View File
@@ -0,0 +1,261 @@
// This file is part of Substrate.
// Copyright (C) 2020 Parity Technologies (UK) Ltd.
// 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.
//! Tests for the module.
use super::*;
use mock::{
Lottery, Balances, Test, Origin, Call, SystemCall, BalancesCall,
new_test_ext, run_to_block
};
use sp_runtime::traits::{BadOrigin};
use frame_support::{assert_noop, assert_ok};
use pallet_balances::Error as BalancesError;
#[test]
fn initial_state() {
new_test_ext().execute_with(|| {
assert_eq!(Balances::free_balance(Lottery::account_id()), 0);
assert!(crate::Lottery::<Test>::get().is_none());
assert_eq!(Participants::<Test>::get(&1), (0, vec![]));
assert_eq!(TicketsCount::get(), 0);
assert!(Tickets::<Test>::get(0).is_none());
});
}
#[test]
fn basic_end_to_end_works() {
new_test_ext().execute_with(|| {
let price = 10;
let length = 20;
let delay = 5;
let calls = vec![
Call::Balances(BalancesCall::force_transfer(0, 0, 0)),
Call::Balances(BalancesCall::transfer(0, 0)),
];
// Set calls for the lottery
assert_ok!(Lottery::set_calls(Origin::root(), calls));
// Start lottery, it repeats
assert_ok!(Lottery::start_lottery(Origin::root(), price, length, delay, true));
assert!(crate::Lottery::<Test>::get().is_some());
assert_eq!(Balances::free_balance(&1), 100);
let call = Box::new(Call::Balances(BalancesCall::transfer(2, 20)));
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call.clone()));
// 20 from the transfer, 10 from buying a ticket
assert_eq!(Balances::free_balance(&1), 100 - 20 - 10);
assert_eq!(Participants::<Test>::get(&1).1.len(), 1);
assert_eq!(TicketsCount::get(), 1);
// 1 owns the 0 ticket
assert_eq!(Tickets::<Test>::get(0), Some(1));
// More ticket purchases
assert_ok!(Lottery::buy_ticket(Origin::signed(2), call.clone()));
assert_ok!(Lottery::buy_ticket(Origin::signed(3), call.clone()));
assert_ok!(Lottery::buy_ticket(Origin::signed(4), call.clone()));
assert_eq!(TicketsCount::get(), 4);
// Go to end
run_to_block(20);
assert_ok!(Lottery::buy_ticket(Origin::signed(5), call.clone()));
// Ticket isn't bought
assert_eq!(TicketsCount::get(), 4);
// Go to payout
run_to_block(25);
// User 1 wins
assert_eq!(Balances::free_balance(&1), 70 + 40);
// Lottery is reset and restarted
assert_eq!(TicketsCount::get(), 0);
assert_eq!(LotteryIndex::get(), 2);
assert_eq!(
crate::Lottery::<Test>::get().unwrap(),
LotteryConfig {
price,
start: 25,
length,
delay,
repeat: true,
}
);
});
}
#[test]
fn set_calls_works() {
new_test_ext().execute_with(|| {
assert!(!CallIndices::exists());
let calls = vec![
Call::Balances(BalancesCall::force_transfer(0, 0, 0)),
Call::Balances(BalancesCall::transfer(0, 0)),
];
assert_ok!(Lottery::set_calls(Origin::root(), calls));
assert!(CallIndices::exists());
let too_many_calls = vec![
Call::Balances(BalancesCall::force_transfer(0, 0, 0)),
Call::Balances(BalancesCall::transfer(0, 0)),
Call::System(SystemCall::remark(vec![])),
];
assert_noop!(
Lottery::set_calls(Origin::root(), too_many_calls),
Error::<Test>::TooManyCalls,
);
// Clear calls
assert_ok!(Lottery::set_calls(Origin::root(), vec![]));
assert!(CallIndices::get().is_empty());
});
}
#[test]
fn start_lottery_works() {
new_test_ext().execute_with(|| {
let price = 10;
let length = 20;
let delay = 5;
// Setup ignores bad origin
assert_noop!(
Lottery::start_lottery(Origin::signed(1), price, length, delay, false),
BadOrigin,
);
// All good
assert_ok!(Lottery::start_lottery(Origin::root(), price, length, delay, false));
// Can't open another one if lottery is already present
assert_noop!(
Lottery::start_lottery(Origin::root(), price, length, delay, false),
Error::<Test>::InProgress,
);
});
}
#[test]
fn buy_ticket_works_as_simple_passthrough() {
// This test checks that even if the user could not buy a ticket, that `buy_ticket` acts
// as a simple passthrough to the real call.
new_test_ext().execute_with(|| {
// No lottery set up
let call = Box::new(Call::Balances(BalancesCall::transfer(2, 20)));
// This is just a basic transfer then
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call.clone()));
assert_eq!(Balances::free_balance(&1), 100 - 20);
assert_eq!(TicketsCount::get(), 0);
// Lottery is set up, but too expensive to enter, so `do_buy_ticket` fails.
let calls = vec![
Call::Balances(BalancesCall::force_transfer(0, 0, 0)),
Call::Balances(BalancesCall::transfer(0, 0)),
];
assert_ok!(Lottery::set_calls(Origin::root(), calls));
// Ticket price of 60 would kill the user's account
assert_ok!(Lottery::start_lottery(Origin::root(), 60, 10, 5, false));
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call.clone()));
assert_eq!(Balances::free_balance(&1), 100 - 20 - 20);
assert_eq!(TicketsCount::get(), 0);
// If call would fail, the whole thing still fails the same
let fail_call = Box::new(Call::Balances(BalancesCall::transfer(2, 1000)));
assert_noop!(
Lottery::buy_ticket(Origin::signed(1), fail_call),
BalancesError::<Test, _>::InsufficientBalance,
);
let bad_origin_call = Box::new(Call::Balances(BalancesCall::force_transfer(0, 0, 0)));
assert_noop!(
Lottery::buy_ticket(Origin::signed(1), bad_origin_call),
BadOrigin,
);
// User can call other txs, but doesn't get a ticket
let remark_call = Box::new(Call::System(SystemCall::remark(b"hello, world!".to_vec())));
assert_ok!(Lottery::buy_ticket(Origin::signed(2), remark_call));
assert_eq!(TicketsCount::get(), 0);
let successful_call = Box::new(Call::Balances(BalancesCall::transfer(2, 1)));
assert_ok!(Lottery::buy_ticket(Origin::signed(2), successful_call));
assert_eq!(TicketsCount::get(), 1);
});
}
#[test]
fn buy_ticket_works() {
new_test_ext().execute_with(|| {
// Set calls for the lottery.
let calls = vec![
Call::System(SystemCall::remark(vec![])),
Call::Balances(BalancesCall::transfer(0, 0)),
];
assert_ok!(Lottery::set_calls(Origin::root(), calls));
// Can't buy ticket before start
let call = Box::new(Call::Balances(BalancesCall::transfer(2, 1)));
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call.clone()));
assert_eq!(TicketsCount::get(), 0);
// Start lottery
assert_ok!(Lottery::start_lottery(Origin::root(), 1, 20, 5, false));
// Go to start, buy ticket for transfer
run_to_block(5);
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call));
assert_eq!(TicketsCount::get(), 1);
// Can't buy another of the same ticket (even if call is slightly changed)
let call = Box::new(Call::Balances(BalancesCall::transfer(3, 30)));
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call));
assert_eq!(TicketsCount::get(), 1);
// Buy ticket for remark
let call = Box::new(Call::System(SystemCall::remark(b"hello, world!".to_vec())));
assert_ok!(Lottery::buy_ticket(Origin::signed(1), call.clone()));
assert_eq!(TicketsCount::get(), 2);
// Go to end, can't buy tickets anymore
run_to_block(20);
assert_ok!(Lottery::buy_ticket(Origin::signed(2), call.clone()));
assert_eq!(TicketsCount::get(), 2);
// Go to payout, can't buy tickets when there is no lottery open
run_to_block(25);
assert_ok!(Lottery::buy_ticket(Origin::signed(2), call.clone()));
assert_eq!(TicketsCount::get(), 0);
assert_eq!(LotteryIndex::get(), 1);
});
}
#[test]
fn start_lottery_will_create_account() {
new_test_ext().execute_with(|| {
let price = 10;
let length = 20;
let delay = 5;
assert_eq!(Balances::total_balance(&Lottery::account_id()), 0);
assert_ok!(Lottery::start_lottery(Origin::root(), price, length, delay, false));
assert_eq!(Balances::total_balance(&Lottery::account_id()), 1);
});
}
+124
View File
@@ -0,0 +1,124 @@
// This file is part of Substrate.
// Copyright (C) 2021 Parity Technologies (UK) Ltd.
// 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.
//! Autogenerated weights for pallet_lottery
//!
//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 2.0.0
//! DATE: 2021-01-05, STEPS: [50, ], REPEAT: 20, LOW RANGE: [], HIGH RANGE: []
//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("dev"), DB CACHE: 128
// Executed Command:
// target/release/substrate
// benchmark
// --chain=dev
// --steps=50
// --repeat=20
// --pallet=pallet_lottery
// --extrinsic=*
// --execution=wasm
// --wasm-execution=compiled
// --heap-pages=4096
// --output=./frame/lottery/src/weights.rs
// --template=./.maintain/frame-weight-template.hbs
#![allow(unused_parens)]
#![allow(unused_imports)]
use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}};
use sp_std::marker::PhantomData;
/// Weight functions needed for pallet_lottery.
pub trait WeightInfo {
fn buy_ticket() -> Weight;
fn set_calls(n: u32, ) -> Weight;
fn start_lottery() -> Weight;
fn stop_repeat() -> Weight;
fn on_initialize_end() -> Weight;
fn on_initialize_repeat() -> Weight;
}
/// Weights for pallet_lottery using the Substrate node and recommended hardware.
pub struct SubstrateWeight<T>(PhantomData<T>);
impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
fn buy_ticket() -> Weight {
(97_799_000 as Weight)
.saturating_add(T::DbWeight::get().reads(6 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
}
fn set_calls(n: u32, ) -> Weight {
(20_932_000 as Weight)
// Standard Error: 9_000
.saturating_add((513_000 as Weight).saturating_mul(n as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn start_lottery() -> Weight {
(77_600_000 as Weight)
.saturating_add(T::DbWeight::get().reads(3 as Weight))
.saturating_add(T::DbWeight::get().writes(3 as Weight))
}
fn stop_repeat() -> Weight {
(10_707_000 as Weight)
.saturating_add(T::DbWeight::get().reads(1 as Weight))
.saturating_add(T::DbWeight::get().writes(1 as Weight))
}
fn on_initialize_end() -> Weight {
(162_126_000 as Weight)
.saturating_add(T::DbWeight::get().reads(6 as Weight))
.saturating_add(T::DbWeight::get().writes(4 as Weight))
}
fn on_initialize_repeat() -> Weight {
(169_310_000 as Weight)
.saturating_add(T::DbWeight::get().reads(7 as Weight))
.saturating_add(T::DbWeight::get().writes(5 as Weight))
}
}
// For backwards compatibility and tests
impl WeightInfo for () {
fn buy_ticket() -> Weight {
(97_799_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
}
fn set_calls(n: u32, ) -> Weight {
(20_932_000 as Weight)
// Standard Error: 9_000
.saturating_add((513_000 as Weight).saturating_mul(n as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn start_lottery() -> Weight {
(77_600_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(3 as Weight))
.saturating_add(RocksDbWeight::get().writes(3 as Weight))
}
fn stop_repeat() -> Weight {
(10_707_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(1 as Weight))
.saturating_add(RocksDbWeight::get().writes(1 as Weight))
}
fn on_initialize_end() -> Weight {
(162_126_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(6 as Weight))
.saturating_add(RocksDbWeight::get().writes(4 as Weight))
}
fn on_initialize_repeat() -> Weight {
(169_310_000 as Weight)
.saturating_add(RocksDbWeight::get().reads(7 as Weight))
.saturating_add(RocksDbWeight::get().writes(5 as Weight))
}
}