From 811e7e74e68ace34475a76312df767ba40299e45 Mon Sep 17 00:00:00 2001 From: Krayt78 Date: Sat, 7 Dec 2024 16:14:25 +0100 Subject: [PATCH 1/2] extracted mod data to new benchmarking/crypto/mock/tests files --- .../common/src/crowdloan/benchmarking.rs | 296 ++++ .../runtime/common/src/crowdloan/crypto.rs | 33 + polkadot/runtime/common/src/crowdloan/mock.rs | 283 ++++ polkadot/runtime/common/src/crowdloan/mod.rs | 1409 +---------------- .../runtime/common/src/crowdloan/tests.rs | 846 ++++++++++ 5 files changed, 1463 insertions(+), 1404 deletions(-) create mode 100644 polkadot/runtime/common/src/crowdloan/benchmarking.rs create mode 100644 polkadot/runtime/common/src/crowdloan/crypto.rs create mode 100644 polkadot/runtime/common/src/crowdloan/mock.rs create mode 100644 polkadot/runtime/common/src/crowdloan/tests.rs diff --git a/polkadot/runtime/common/src/crowdloan/benchmarking.rs b/polkadot/runtime/common/src/crowdloan/benchmarking.rs new file mode 100644 index 000000000000..5fdab63227bd --- /dev/null +++ b/polkadot/runtime/common/src/crowdloan/benchmarking.rs @@ -0,0 +1,296 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Benchmarking for crowdloan pallet + +#[cfg(feature = "runtime-benchmarks")] +use super::{Pallet as Crowdloan, *}; +use frame_support::{assert_ok, traits::OnInitialize}; +use frame_system::RawOrigin; +use polkadot_runtime_parachains::paras; +use sp_core::crypto::UncheckedFrom; +use sp_runtime::traits::{Bounded, CheckedSub}; + +use frame_benchmarking::{account, benchmarks, whitelisted_caller}; + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +fn create_fund(id: u32, end: BlockNumberFor) -> ParaId { + let cap = BalanceOf::::max_value(); + let (_, offset) = T::Auctioneer::lease_period_length(); + // Set to the very beginning of lease period index 0. + frame_system::Pallet::::set_block_number(offset); + let now = frame_system::Pallet::::block_number(); + let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default(); + let first_period = lease_period_index; + let last_period = lease_period_index + ((SlotRange::LEASE_PERIODS_PER_SLOT as u32) - 1).into(); + let para_id = id.into(); + + let caller = account("fund_creator", id, 0); + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + + // Assume ed25519 is most complex signature format + let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + + let head_data = T::Registrar::worst_head_data(); + let validation_code = T::Registrar::worst_validation_code(); + assert_ok!(T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())); + assert_ok!(paras::Pallet::::add_trusted_validation_code( + frame_system::Origin::::Root.into(), + validation_code, + )); + T::Registrar::execute_pending_transitions(); + + assert_ok!(Crowdloan::::create( + RawOrigin::Signed(caller).into(), + para_id, + cap, + first_period, + last_period, + end, + Some(pubkey) + )); + + para_id +} + +fn contribute_fund(who: &T::AccountId, index: ParaId) { + CurrencyOf::::make_free_balance_be(&who, BalanceOf::::max_value()); + let value = T::MinContribution::get(); + + let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + let payload = (index, &who, BalanceOf::::default(), value); + let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey); + + assert_ok!(Crowdloan::::contribute( + RawOrigin::Signed(who.clone()).into(), + index, + value, + Some(sig) + )); +} + +benchmarks! { + where_clause { where T: paras::Config } + + create { + let para_id = ParaId::from(1_u32); + let cap = BalanceOf::::max_value(); + let first_period = 0u32.into(); + let last_period = 3u32.into(); + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + + let caller: T::AccountId = whitelisted_caller(); + let head_data = T::Registrar::worst_head_data(); + let validation_code = T::Registrar::worst_validation_code(); + + let verifier = MultiSigner::unchecked_from(account::<[u8; 32]>("verifier", 0, 0)); + + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())?; + assert_ok!(paras::Pallet::::add_trusted_validation_code( + frame_system::Origin::::Root.into(), + validation_code, + )); + + T::Registrar::execute_pending_transitions(); + + }: _(RawOrigin::Signed(caller), para_id, cap, first_period, last_period, end, Some(verifier)) + verify { + assert_last_event::(Event::::Created { para_id }.into()) + } + + // Contribute has two arms: PreEnding and Ending, but both are equal complexity. + contribute { + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + let fund_index = create_fund::(1, end); + let caller: T::AccountId = whitelisted_caller(); + let contribution = T::MinContribution::get(); + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + assert!(NewRaise::::get().is_empty()); + + let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + let payload = (fund_index, &caller, BalanceOf::::default(), contribution); + let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey); + + }: _(RawOrigin::Signed(caller.clone()), fund_index, contribution, Some(sig)) + verify { + // NewRaise is appended to, so we don't need to fill it up for worst case scenario. + assert!(!NewRaise::::get().is_empty()); + assert_last_event::(Event::::Contributed { who: caller, fund_index, amount: contribution }.into()); + } + + withdraw { + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + let fund_index = create_fund::(1337, end); + let caller: T::AccountId = whitelisted_caller(); + let contributor = account("contributor", 0, 0); + contribute_fund::(&contributor, fund_index); + frame_system::Pallet::::set_block_number(BlockNumberFor::::max_value()); + }: _(RawOrigin::Signed(caller), contributor.clone(), fund_index) + verify { + assert_last_event::(Event::::Withdrew { who: contributor, fund_index, amount: T::MinContribution::get() }.into()); + } + + // Worst case: Refund removes `RemoveKeysLimit` keys, and is fully refunded. + #[skip_meta] + refund { + let k in 0 .. T::RemoveKeysLimit::get(); + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + let fund_index = create_fund::(1337, end); + + // Dissolve will remove at most `RemoveKeysLimit` at once. + for i in 0 .. k { + contribute_fund::(&account("contributor", i, 0), fund_index); + } + + let caller: T::AccountId = whitelisted_caller(); + frame_system::Pallet::::set_block_number(BlockNumberFor::::max_value()); + }: _(RawOrigin::Signed(caller), fund_index) + verify { + assert_last_event::(Event::::AllRefunded { para_id: fund_index }.into()); + } + + dissolve { + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + let fund_index = create_fund::(1337, end); + let caller: T::AccountId = whitelisted_caller(); + frame_system::Pallet::::set_block_number(BlockNumberFor::::max_value()); + }: _(RawOrigin::Signed(caller.clone()), fund_index) + verify { + assert_last_event::(Event::::Dissolved { para_id: fund_index }.into()); + } + + edit { + let para_id = ParaId::from(1_u32); + let cap = BalanceOf::::max_value(); + let first_period = 0u32.into(); + let last_period = 3u32.into(); + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + + let caller: T::AccountId = whitelisted_caller(); + let head_data = T::Registrar::worst_head_data(); + let validation_code = T::Registrar::worst_validation_code(); + + let verifier = MultiSigner::unchecked_from(account::<[u8; 32]>("verifier", 0, 0)); + + CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); + T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())?; + assert_ok!(paras::Pallet::::add_trusted_validation_code( + frame_system::Origin::::Root.into(), + validation_code, + )); + + T::Registrar::execute_pending_transitions(); + + Crowdloan::::create( + RawOrigin::Signed(caller).into(), + para_id, cap, first_period, last_period, end, Some(verifier.clone()), + )?; + + // Doesn't matter what we edit to, so use the same values. + }: _(RawOrigin::Root, para_id, cap, first_period, last_period, end, Some(verifier)) + verify { + assert_last_event::(Event::::Edited { para_id }.into()) + } + + add_memo { + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + let fund_index = create_fund::(1, end); + let caller: T::AccountId = whitelisted_caller(); + contribute_fund::(&caller, fund_index); + let worst_memo = vec![42; T::MaxMemoLength::get().into()]; + }: _(RawOrigin::Signed(caller.clone()), fund_index, worst_memo.clone()) + verify { + let fund = Funds::::get(fund_index).expect("fund was created..."); + assert_eq!( + Crowdloan::::contribution_get(fund.fund_index, &caller), + (T::MinContribution::get(), worst_memo), + ); + } + + poke { + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end = lpl + offset; + let fund_index = create_fund::(1, end); + let caller: T::AccountId = whitelisted_caller(); + contribute_fund::(&caller, fund_index); + NewRaise::::kill(); + assert!(NewRaise::::get().is_empty()); + }: _(RawOrigin::Signed(caller), fund_index) + verify { + assert!(!NewRaise::::get().is_empty()); + assert_last_event::(Event::::AddedToNewRaise { para_id: fund_index }.into()) + } + + // Worst case scenario: N funds are all in the `NewRaise` list, we are + // in the beginning of the ending period, and each fund outbids the next + // over the same periods. + on_initialize { + // We test the complexity over different number of new raise + let n in 2 .. 100; + let (lpl, offset) = T::Auctioneer::lease_period_length(); + let end_block = lpl + offset - 1u32.into(); + + let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + + for i in 0 .. n { + let fund_index = create_fund::(i, end_block); + let contributor: T::AccountId = account("contributor", i, 0); + let contribution = T::MinContribution::get() * (i + 1).into(); + let payload = (fund_index, &contributor, BalanceOf::::default(), contribution); + let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey.clone()); + + CurrencyOf::::make_free_balance_be(&contributor, BalanceOf::::max_value()); + Crowdloan::::contribute(RawOrigin::Signed(contributor).into(), fund_index, contribution, Some(sig))?; + } + + let now = frame_system::Pallet::::block_number(); + let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default(); + let duration = end_block + .checked_sub(&frame_system::Pallet::::block_number()) + .ok_or("duration of auction less than zero")?; + T::Auctioneer::new_auction(duration, lease_period_index)?; + + assert_eq!(T::Auctioneer::auction_status(end_block).is_ending(), Some((0u32.into(), 0u32.into()))); + assert_eq!(NewRaise::::get().len(), n as usize); + let old_endings_count = EndingsCount::::get(); + }: { + Crowdloan::::on_initialize(end_block); + } verify { + assert_eq!(EndingsCount::::get(), old_endings_count + 1); + assert_last_event::(Event::::HandleBidResult { para_id: (n - 1).into(), result: Ok(()) }.into()); + } + + impl_benchmark_test_suite!( + Crowdloan, + crate::integration_tests::new_test_ext_with_offset(10), + crate::integration_tests::Test, + ); +} diff --git a/polkadot/runtime/common/src/crowdloan/crypto.rs b/polkadot/runtime/common/src/crowdloan/crypto.rs new file mode 100644 index 000000000000..6ccd19be433b --- /dev/null +++ b/polkadot/runtime/common/src/crowdloan/crypto.rs @@ -0,0 +1,33 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Crypto utilities for testing and benchmarking in crowdloan pallet. + +#[cfg(any(feature = "runtime-benchmarks", test))] +use alloc::vec::Vec; +use sp_core::ed25519; +use sp_io::crypto::{ed25519_generate, ed25519_sign}; +use sp_runtime::{MultiSignature, MultiSigner}; + +pub fn create_ed25519_pubkey(seed: Vec) -> MultiSigner { + ed25519_generate(0.into(), Some(seed)).into() +} + +pub fn create_ed25519_signature(payload: &[u8], pubkey: MultiSigner) -> MultiSignature { + let edpubkey = ed25519::Public::try_from(pubkey).unwrap(); + let edsig = ed25519_sign(0.into(), &edpubkey, payload).unwrap(); + edsig.into() +} diff --git a/polkadot/runtime/common/src/crowdloan/mock.rs b/polkadot/runtime/common/src/crowdloan/mock.rs new file mode 100644 index 000000000000..afc8e276af28 --- /dev/null +++ b/polkadot/runtime/common/src/crowdloan/mock.rs @@ -0,0 +1,283 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Mocking utilities for testing in crowdloan pallet. + +#[cfg(test)] +use super::*; + +use frame_support::{ + assert_ok, derive_impl, parameter_types, + traits::{OnFinalize, OnInitialize}, +}; +use polkadot_primitives::Id as ParaId; +use sp_core::H256; +use std::{cell::RefCell, collections::BTreeMap, sync::Arc}; +// The testing primitives are very useful for avoiding having to work with signatures +// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required. +use crate::{crowdloan, mock::TestRegistrar, traits::AuctionStatus}; +use polkadot_primitives_test_helpers::{dummy_head_data, dummy_validation_code}; +use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, DispatchResult, +}; + +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Crowdloan: crowdloan, + } +); + +type BlockNumber = u64; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub struct BidPlaced { + pub height: u64, + pub bidder: u64, + pub para: ParaId, + pub first_period: u64, + pub last_period: u64, + pub amount: u64, +} +thread_local! { + static AUCTION: RefCell> = RefCell::new(None); + static VRF_DELAY: RefCell = RefCell::new(0); + static ENDING_PERIOD: RefCell = RefCell::new(5); + static BIDS_PLACED: RefCell> = RefCell::new(Vec::new()); + static HAS_WON: RefCell> = RefCell::new(BTreeMap::new()); +} + +#[allow(unused)] +pub fn set_ending_period(ending_period: u64) { + ENDING_PERIOD.with(|p| *p.borrow_mut() = ending_period); +} +pub fn auction() -> Option<(u64, u64)> { + AUCTION.with(|p| *p.borrow()) +} +pub fn ending_period() -> u64 { + ENDING_PERIOD.with(|p| *p.borrow()) +} +pub fn bids() -> Vec { + BIDS_PLACED.with(|p| p.borrow().clone()) +} +pub fn vrf_delay() -> u64 { + VRF_DELAY.with(|p| *p.borrow()) +} +pub fn set_vrf_delay(delay: u64) { + VRF_DELAY.with(|p| *p.borrow_mut() = delay); +} +// Emulate what would happen if we won an auction: +// balance is reserved and a deposit_held is recorded +pub fn set_winner(para: ParaId, who: u64, winner: bool) { + let fund = Funds::::get(para).unwrap(); + let account_id = Crowdloan::fund_account_id(fund.fund_index); + if winner { + let ed: u64 = ::ExistentialDeposit::get(); + let free_balance = Balances::free_balance(&account_id); + Balances::reserve(&account_id, free_balance - ed) + .expect("should be able to reserve free balance minus ED"); + } else { + let reserved_balance = Balances::reserved_balance(&account_id); + Balances::unreserve(&account_id, reserved_balance); + } + HAS_WON.with(|p| p.borrow_mut().insert((para, who), winner)); +} + +pub struct TestAuctioneer; +impl Auctioneer for TestAuctioneer { + type AccountId = u64; + type LeasePeriod = u64; + type Currency = Balances; + + fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult { + let now = System::block_number(); + let (current_lease_period, _) = + Self::lease_period_index(now).ok_or("no lease period yet")?; + assert!(lease_period_index >= current_lease_period); + + let ending = System::block_number().saturating_add(duration); + AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending))); + Ok(()) + } + + fn auction_status(now: u64) -> AuctionStatus { + let early_end = match auction() { + Some((_, early_end)) => early_end, + None => return AuctionStatus::NotStarted, + }; + let after_early_end = match now.checked_sub(early_end) { + Some(after_early_end) => after_early_end, + None => return AuctionStatus::StartingPeriod, + }; + + let ending_period = ending_period(); + if after_early_end < ending_period { + return AuctionStatus::EndingPeriod(after_early_end, 0) + } else { + let after_end = after_early_end - ending_period; + // Optional VRF delay + if after_end < vrf_delay() { + return AuctionStatus::VrfDelay(after_end) + } else { + // VRF delay is done, so we just end the auction + return AuctionStatus::NotStarted + } + } + } + + fn place_bid( + bidder: u64, + para: ParaId, + first_period: u64, + last_period: u64, + amount: u64, + ) -> DispatchResult { + let height = System::block_number(); + BIDS_PLACED.with(|p| { + p.borrow_mut().push(BidPlaced { + height, + bidder, + para, + first_period, + last_period, + amount, + }) + }); + Ok(()) + } + + fn lease_period_index(b: BlockNumber) -> Option<(u64, bool)> { + let (lease_period_length, offset) = Self::lease_period_length(); + let b = b.checked_sub(offset)?; + + let lease_period = b / lease_period_length; + let first_block = (b % lease_period_length).is_zero(); + Some((lease_period, first_block)) + } + + fn lease_period_length() -> (u64, u64) { + (20, 0) + } + + fn has_won_an_auction(para: ParaId, bidder: &u64) -> bool { + HAS_WON.with(|p| *p.borrow().get(&(para, *bidder)).unwrap_or(&false)) + } +} + +parameter_types! { + pub const SubmissionDeposit: u64 = 1; + pub const MinContribution: u64 = 10; + pub const CrowdloanPalletId: PalletId = PalletId(*b"py/cfund"); + pub const RemoveKeysLimit: u32 = 10; + pub const MaxMemoLength: u8 = 32; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type SubmissionDeposit = SubmissionDeposit; + type MinContribution = MinContribution; + type PalletId = CrowdloanPalletId; + type RemoveKeysLimit = RemoveKeysLimit; + type Registrar = TestRegistrar; + type Auctioneer = TestAuctioneer; + type MaxMemoLength = MaxMemoLength; + type WeightInfo = crate::crowdloan::TestWeightInfo; +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)], + } + .assimilate_storage(&mut t) + .unwrap(); + let keystore = MemoryKeystore::new(); + let mut t: sp_io::TestExternalities = t.into(); + t.register_extension(KeystoreExt(Arc::new(keystore))); + t +} + +pub fn new_para() -> ParaId { + for i in 0.. { + let para: ParaId = i.into(); + if TestRegistrar::::is_registered(para) { + continue + } + assert_ok!(TestRegistrar::::register( + 1, + para, + dummy_head_data(), + dummy_validation_code() + )); + return para + } + unreachable!() +} + +pub fn run_to_block(n: u64) { + while System::block_number() < n { + Crowdloan::on_finalize(System::block_number()); + Balances::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()); + Balances::on_initialize(System::block_number()); + Crowdloan::on_initialize(System::block_number()); + } +} + +pub fn last_event() -> RuntimeEvent { + System::events().pop().expect("RuntimeEvent expected").event +} diff --git a/polkadot/runtime/common/src/crowdloan/mod.rs b/polkadot/runtime/common/src/crowdloan/mod.rs index 8cf288197e3d..3aef69b25fa1 100644 --- a/polkadot/runtime/common/src/crowdloan/mod.rs +++ b/polkadot/runtime/common/src/crowdloan/mod.rs @@ -837,1412 +837,13 @@ impl crate::traits::OnSwap for Pallet { } #[cfg(any(feature = "runtime-benchmarks", test))] -mod crypto { - use alloc::vec::Vec; - use sp_core::ed25519; - use sp_io::crypto::{ed25519_generate, ed25519_sign}; - use sp_runtime::{MultiSignature, MultiSigner}; - - pub fn create_ed25519_pubkey(seed: Vec) -> MultiSigner { - ed25519_generate(0.into(), Some(seed)).into() - } - - pub fn create_ed25519_signature(payload: &[u8], pubkey: MultiSigner) -> MultiSignature { - let edpubkey = ed25519::Public::try_from(pubkey).unwrap(); - let edsig = ed25519_sign(0.into(), &edpubkey, payload).unwrap(); - edsig.into() - } -} +mod crypto; #[cfg(test)] -mod tests { - use super::*; - - use frame_support::{ - assert_noop, assert_ok, derive_impl, parameter_types, - traits::{OnFinalize, OnInitialize}, - }; - use polkadot_primitives::Id as ParaId; - use sp_core::H256; - use std::{cell::RefCell, collections::BTreeMap, sync::Arc}; - // The testing primitives are very useful for avoiding having to work with signatures - // or public keys. `u64` is used as the `AccountId` and no `Signature`s are required. - use crate::{ - crowdloan, - mock::TestRegistrar, - traits::{AuctionStatus, OnSwap}, - }; - use polkadot_primitives_test_helpers::{dummy_head_data, dummy_validation_code}; - use sp_keystore::{testing::MemoryKeystore, KeystoreExt}; - use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup, TrailingZeroInput}, - BuildStorage, DispatchResult, - }; - - type Block = frame_system::mocking::MockBlock; - - frame_support::construct_runtime!( - pub enum Test - { - System: frame_system, - Balances: pallet_balances, - Crowdloan: crowdloan, - } - ); - - type BlockNumber = u64; - - #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] - impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Block = Block; - type RuntimeEvent = RuntimeEvent; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; - } - - #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] - impl pallet_balances::Config for Test { - type AccountStore = System; - } - - #[derive(Copy, Clone, Eq, PartialEq, Debug)] - struct BidPlaced { - height: u64, - bidder: u64, - para: ParaId, - first_period: u64, - last_period: u64, - amount: u64, - } - thread_local! { - static AUCTION: RefCell> = RefCell::new(None); - static VRF_DELAY: RefCell = RefCell::new(0); - static ENDING_PERIOD: RefCell = RefCell::new(5); - static BIDS_PLACED: RefCell> = RefCell::new(Vec::new()); - static HAS_WON: RefCell> = RefCell::new(BTreeMap::new()); - } - - #[allow(unused)] - fn set_ending_period(ending_period: u64) { - ENDING_PERIOD.with(|p| *p.borrow_mut() = ending_period); - } - fn auction() -> Option<(u64, u64)> { - AUCTION.with(|p| *p.borrow()) - } - fn ending_period() -> u64 { - ENDING_PERIOD.with(|p| *p.borrow()) - } - fn bids() -> Vec { - BIDS_PLACED.with(|p| p.borrow().clone()) - } - fn vrf_delay() -> u64 { - VRF_DELAY.with(|p| *p.borrow()) - } - fn set_vrf_delay(delay: u64) { - VRF_DELAY.with(|p| *p.borrow_mut() = delay); - } - // Emulate what would happen if we won an auction: - // balance is reserved and a deposit_held is recorded - fn set_winner(para: ParaId, who: u64, winner: bool) { - let fund = Funds::::get(para).unwrap(); - let account_id = Crowdloan::fund_account_id(fund.fund_index); - if winner { - let ed: u64 = ::ExistentialDeposit::get(); - let free_balance = Balances::free_balance(&account_id); - Balances::reserve(&account_id, free_balance - ed) - .expect("should be able to reserve free balance minus ED"); - } else { - let reserved_balance = Balances::reserved_balance(&account_id); - Balances::unreserve(&account_id, reserved_balance); - } - HAS_WON.with(|p| p.borrow_mut().insert((para, who), winner)); - } - - pub struct TestAuctioneer; - impl Auctioneer for TestAuctioneer { - type AccountId = u64; - type LeasePeriod = u64; - type Currency = Balances; - - fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult { - let now = System::block_number(); - let (current_lease_period, _) = - Self::lease_period_index(now).ok_or("no lease period yet")?; - assert!(lease_period_index >= current_lease_period); - - let ending = System::block_number().saturating_add(duration); - AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending))); - Ok(()) - } - - fn auction_status(now: u64) -> AuctionStatus { - let early_end = match auction() { - Some((_, early_end)) => early_end, - None => return AuctionStatus::NotStarted, - }; - let after_early_end = match now.checked_sub(early_end) { - Some(after_early_end) => after_early_end, - None => return AuctionStatus::StartingPeriod, - }; - - let ending_period = ending_period(); - if after_early_end < ending_period { - return AuctionStatus::EndingPeriod(after_early_end, 0) - } else { - let after_end = after_early_end - ending_period; - // Optional VRF delay - if after_end < vrf_delay() { - return AuctionStatus::VrfDelay(after_end) - } else { - // VRF delay is done, so we just end the auction - return AuctionStatus::NotStarted - } - } - } - - fn place_bid( - bidder: u64, - para: ParaId, - first_period: u64, - last_period: u64, - amount: u64, - ) -> DispatchResult { - let height = System::block_number(); - BIDS_PLACED.with(|p| { - p.borrow_mut().push(BidPlaced { - height, - bidder, - para, - first_period, - last_period, - amount, - }) - }); - Ok(()) - } - - fn lease_period_index(b: BlockNumber) -> Option<(u64, bool)> { - let (lease_period_length, offset) = Self::lease_period_length(); - let b = b.checked_sub(offset)?; - - let lease_period = b / lease_period_length; - let first_block = (b % lease_period_length).is_zero(); - Some((lease_period, first_block)) - } - - fn lease_period_length() -> (u64, u64) { - (20, 0) - } - - fn has_won_an_auction(para: ParaId, bidder: &u64) -> bool { - HAS_WON.with(|p| *p.borrow().get(&(para, *bidder)).unwrap_or(&false)) - } - } - - parameter_types! { - pub const SubmissionDeposit: u64 = 1; - pub const MinContribution: u64 = 10; - pub const CrowdloanPalletId: PalletId = PalletId(*b"py/cfund"); - pub const RemoveKeysLimit: u32 = 10; - pub const MaxMemoLength: u8 = 32; - } - - impl Config for Test { - type RuntimeEvent = RuntimeEvent; - type SubmissionDeposit = SubmissionDeposit; - type MinContribution = MinContribution; - type PalletId = CrowdloanPalletId; - type RemoveKeysLimit = RemoveKeysLimit; - type Registrar = TestRegistrar; - type Auctioneer = TestAuctioneer; - type MaxMemoLength = MaxMemoLength; - type WeightInfo = crate::crowdloan::TestWeightInfo; - } - - use pallet_balances::Error as BalancesError; - - // This function basically just builds a genesis storage key/value store according to - // our desired mockup. - pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)], - } - .assimilate_storage(&mut t) - .unwrap(); - let keystore = MemoryKeystore::new(); - let mut t: sp_io::TestExternalities = t.into(); - t.register_extension(KeystoreExt(Arc::new(keystore))); - t - } - - fn new_para() -> ParaId { - for i in 0.. { - let para: ParaId = i.into(); - if TestRegistrar::::is_registered(para) { - continue - } - assert_ok!(TestRegistrar::::register( - 1, - para, - dummy_head_data(), - dummy_validation_code() - )); - return para - } - unreachable!() - } - - fn run_to_block(n: u64) { - while System::block_number() < n { - Crowdloan::on_finalize(System::block_number()); - Balances::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()); - Balances::on_initialize(System::block_number()); - Crowdloan::on_initialize(System::block_number()); - } - } - - fn last_event() -> RuntimeEvent { - System::events().pop().expect("RuntimeEvent expected").event - } - - #[test] - fn basic_setup_works() { - new_test_ext().execute_with(|| { - assert_eq!(System::block_number(), 0); - assert_eq!(crowdloan::Funds::::get(ParaId::from(0)), None); - let empty: Vec = Vec::new(); - assert_eq!(crowdloan::NewRaise::::get(), empty); - assert_eq!(Crowdloan::contribution_get(0u32, &1).0, 0); - assert_eq!(crowdloan::EndingsCount::::get(), 0); - - assert_ok!(TestAuctioneer::new_auction(5, 0)); - - assert_eq!(bids(), vec![]); - assert_ok!(TestAuctioneer::place_bid(1, 2.into(), 0, 3, 6)); - let b = BidPlaced { - height: 0, - bidder: 1, - para: 2.into(), - first_period: 0, - last_period: 3, - amount: 6, - }; - assert_eq!(bids(), vec![b]); - assert_eq!(TestAuctioneer::auction_status(4), AuctionStatus::::StartingPeriod); - assert_eq!(TestAuctioneer::auction_status(5), AuctionStatus::::EndingPeriod(0, 0)); - assert_eq!(TestAuctioneer::auction_status(9), AuctionStatus::::EndingPeriod(4, 0)); - assert_eq!(TestAuctioneer::auction_status(11), AuctionStatus::::NotStarted); - }); - } - - #[test] - fn create_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - // Now try to create a crowdloan campaign - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None)); - // This is what the initial `fund_info` should look like - let fund_info = FundInfo { - depositor: 1, - verifier: None, - deposit: 1, - raised: 0, - // 5 blocks length + 3 block ending period + 1 starting block - end: 9, - cap: 1000, - last_contribution: LastContribution::Never, - first_period: 1, - last_period: 4, - fund_index: 0, - }; - assert_eq!(crowdloan::Funds::::get(para), Some(fund_info)); - // User has deposit removed from their free balance - assert_eq!(Balances::free_balance(1), 999); - // Deposit is placed in reserved - assert_eq!(Balances::reserved_balance(1), 1); - // No new raise until first contribution - let empty: Vec = Vec::new(); - assert_eq!(crowdloan::NewRaise::::get(), empty); - }); - } - - #[test] - fn create_with_verifier_works() { - new_test_ext().execute_with(|| { - let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - let para = new_para(); - // Now try to create a crowdloan campaign - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(1), - para, - 1000, - 1, - 4, - 9, - Some(pubkey.clone()) - )); - // This is what the initial `fund_info` should look like - let fund_info = FundInfo { - depositor: 1, - verifier: Some(pubkey), - deposit: 1, - raised: 0, - // 5 blocks length + 3 block ending period + 1 starting block - end: 9, - cap: 1000, - last_contribution: LastContribution::Never, - first_period: 1, - last_period: 4, - fund_index: 0, - }; - assert_eq!(crowdloan::Funds::::get(ParaId::from(0)), Some(fund_info)); - // User has deposit removed from their free balance - assert_eq!(Balances::free_balance(1), 999); - // Deposit is placed in reserved - assert_eq!(Balances::reserved_balance(1), 1); - // No new raise until first contribution - let empty: Vec = Vec::new(); - assert_eq!(crowdloan::NewRaise::::get(), empty); - }); - } - - #[test] - fn create_handles_basic_errors() { - new_test_ext().execute_with(|| { - // Now try to create a crowdloan campaign - let para = new_para(); - - let e = Error::::InvalidParaId; - assert_noop!( - Crowdloan::create(RuntimeOrigin::signed(1), 1.into(), 1000, 1, 4, 9, None), - e - ); - // Cannot create a crowdloan with bad lease periods - let e = Error::::LastPeriodBeforeFirstPeriod; - assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 4, 1, 9, None), e); - let e = Error::::LastPeriodTooFarInFuture; - assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 9, 9, None), e); - - // Cannot create a crowdloan without some deposit funds - assert_ok!(TestRegistrar::::register( - 1337, - ParaId::from(1234), - dummy_head_data(), - dummy_validation_code() - )); - let e = BalancesError::::InsufficientBalance; - assert_noop!( - Crowdloan::create( - RuntimeOrigin::signed(1337), - ParaId::from(1234), - 1000, - 1, - 3, - 9, - None - ), - e - ); - - // Cannot create a crowdloan with nonsense end date - // This crowdloan would end in lease period 2, but is bidding for some slot that starts - // in lease period 1. - assert_noop!( - Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 41, None), - Error::::EndTooFarInFuture - ); - }); - } - - #[test] - fn contribute_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None)); - - // No contributions yet - assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0); - - // User 1 contributes to their own crowdloan - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None)); - // User 1 has spent some funds to do this, transfer fees **are** taken - assert_eq!(Balances::free_balance(1), 950); - // Contributions are stored in the trie - assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 49); - // Contributions appear in free balance of crowdloan - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(index)), 49); - // Crowdloan is added to NewRaise - assert_eq!(crowdloan::NewRaise::::get(), vec![para]); - - let fund = crowdloan::Funds::::get(para).unwrap(); - - // Last contribution time recorded - assert_eq!(fund.last_contribution, LastContribution::PreEnding(0)); - assert_eq!(fund.raised, 49); - }); - } - - #[test] - fn contribute_with_verifier_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - // Set up a crowdloan - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(1), - para, - 1000, - 1, - 4, - 9, - Some(pubkey.clone()) - )); - - // No contributions yet - assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0); - - // Missing signature - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None), - Error::::InvalidSignature - ); - - let payload = (0u32, 1u64, 0u64, 49u64); - let valid_signature = - crypto::create_ed25519_signature(&payload.encode(), pubkey.clone()); - let invalid_signature = - MultiSignature::decode(&mut TrailingZeroInput::zeroes()).unwrap(); - - // Invalid signature - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, Some(invalid_signature)), - Error::::InvalidSignature - ); - - // Valid signature wrong parameter - assert_noop!( - Crowdloan::contribute( - RuntimeOrigin::signed(1), - para, - 50, - Some(valid_signature.clone()) - ), - Error::::InvalidSignature - ); - assert_noop!( - Crowdloan::contribute( - RuntimeOrigin::signed(2), - para, - 49, - Some(valid_signature.clone()) - ), - Error::::InvalidSignature - ); - - // Valid signature - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(1), - para, - 49, - Some(valid_signature.clone()) - )); - - // Reuse valid signature - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, Some(valid_signature)), - Error::::InvalidSignature - ); - - let payload_2 = (0u32, 1u64, 49u64, 10u64); - let valid_signature_2 = crypto::create_ed25519_signature(&payload_2.encode(), pubkey); - - // New valid signature - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(1), - para, - 10, - Some(valid_signature_2) - )); - - // Contributions appear in free balance of crowdloan - assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(index)), 59); - - // Contribution amount is correct - let fund = crowdloan::Funds::::get(para).unwrap(); - assert_eq!(fund.raised, 59); - }); - } - - #[test] - fn contribute_handles_basic_errors() { - new_test_ext().execute_with(|| { - let para = new_para(); - - // Cannot contribute to non-existing fund - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None), - Error::::InvalidParaId - ); - // Cannot contribute below minimum contribution - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para, 9, None), - Error::::ContributionTooSmall - ); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 101, None)); - - // Cannot contribute past the limit - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(2), para, 900, None), - Error::::CapExceeded - ); - - // Move past end date - run_to_block(10); - - // Cannot contribute to ended fund - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None), - Error::::ContributionPeriodOver - ); - - // If a crowdloan has already won, it should not allow contributions. - let para_2 = new_para(); - let index = NextFundIndex::::get(); - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_2, 1000, 1, 4, 40, None)); - // Emulate a win by leasing out and putting a deposit. Slots pallet would normally do - // this. - let crowdloan_account = Crowdloan::fund_account_id(index); - set_winner(para_2, crowdloan_account, true); - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para_2, 49, None), - Error::::BidOrLeaseActive - ); - - // Move past lease period 1, should not be allowed to have further contributions with a - // crowdloan that has starting period 1. - let para_3 = new_para(); - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_3, 1000, 1, 4, 40, None)); - run_to_block(40); - let now = System::block_number(); - assert_eq!(TestAuctioneer::lease_period_index(now).unwrap().0, 2); - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(1), para_3, 49, None), - Error::::ContributionPeriodOver - ); - }); - } - - #[test] - fn cannot_contribute_during_vrf() { - new_test_ext().execute_with(|| { - set_vrf_delay(5); - - let para = new_para(); - let first_period = 1; - let last_period = 4; - - assert_ok!(TestAuctioneer::new_auction(5, 0)); - - // Set up a crowdloan - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(1), - para, - 1000, - first_period, - last_period, - 20, - None - )); - - run_to_block(8); - // Can def contribute when auction is running. - assert!(TestAuctioneer::auction_status(System::block_number()).is_ending().is_some()); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None)); - - run_to_block(10); - // Can't contribute when auction is in the VRF delay period. - assert!(TestAuctioneer::auction_status(System::block_number()).is_vrf()); - assert_noop!( - Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None), - Error::::VrfDelayInProgress - ); - - run_to_block(15); - // Its fine to contribute when no auction is running. - assert!(!TestAuctioneer::auction_status(System::block_number()).is_in_progress()); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None)); - }) - } - - #[test] - fn bidding_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - let first_period = 1; - let last_period = 4; - - assert_ok!(TestAuctioneer::new_auction(5, 0)); - - // Set up a crowdloan - assert_ok!(Crowdloan::create( - RuntimeOrigin::signed(1), - para, - 1000, - first_period, - last_period, - 9, - None - )); - let bidder = Crowdloan::fund_account_id(index); - - // Fund crowdloan - run_to_block(1); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - run_to_block(3); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 150, None)); - run_to_block(5); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(4), para, 200, None)); - run_to_block(8); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None)); - run_to_block(10); - - assert_eq!( - bids(), - vec![ - BidPlaced { height: 5, amount: 250, bidder, para, first_period, last_period }, - BidPlaced { height: 6, amount: 450, bidder, para, first_period, last_period }, - BidPlaced { height: 9, amount: 700, bidder, para, first_period, last_period }, - ] - ); - - // Endings count incremented - assert_eq!(crowdloan::EndingsCount::::get(), 1); - }); - } - - #[test] - fn withdraw_from_failed_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); - - run_to_block(10); - let account_id = Crowdloan::fund_account_id(index); - // para has no reserved funds, indicating it did not win the auction. - assert_eq!(Balances::reserved_balance(&account_id), 0); - // but there's still the funds in its balance. - assert_eq!(Balances::free_balance(&account_id), 150); - assert_eq!(Balances::free_balance(2), 1900); - assert_eq!(Balances::free_balance(3), 2950); - - assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para)); - assert_eq!(Balances::free_balance(&account_id), 50); - assert_eq!(Balances::free_balance(2), 2000); - - assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 3, para)); - assert_eq!(Balances::free_balance(&account_id), 0); - assert_eq!(Balances::free_balance(3), 3000); - }); - } - - #[test] - fn withdraw_cannot_be_griefed() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - let issuance = Balances::total_issuance(); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - - run_to_block(10); - let account_id = Crowdloan::fund_account_id(index); - - // user sends the crowdloan funds trying to make an accounting error - assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), account_id, 10)); - - // overfunded now - assert_eq!(Balances::free_balance(&account_id), 110); - assert_eq!(Balances::free_balance(2), 1900); - - assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para)); - assert_eq!(Balances::free_balance(2), 2000); - - // Some funds are left over - assert_eq!(Balances::free_balance(&account_id), 10); - // Remaining funds will be burned - assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); - assert_eq!(Balances::free_balance(&account_id), 0); - assert_eq!(Balances::total_issuance(), issuance - 10); - }); - } - - #[test] - fn refund_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - let account_id = Crowdloan::fund_account_id(index); - - // Set up a crowdloan ending on 9 - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - // Make some contributions - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 200, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 300, None)); - - assert_eq!(Balances::free_balance(account_id), 600); - - // Can't refund before the crowdloan it has ended - assert_noop!( - Crowdloan::refund(RuntimeOrigin::signed(1337), para), - Error::::FundNotEnded, - ); - - // Move to the end of the crowdloan - run_to_block(10); - assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para)); - - // Funds are returned - assert_eq!(Balances::free_balance(account_id), 0); - // 1 deposit for the crowdloan which hasn't dissolved yet. - assert_eq!(Balances::free_balance(1), 1000 - 1); - assert_eq!(Balances::free_balance(2), 2000); - assert_eq!(Balances::free_balance(3), 3000); - }); - } - - #[test] - fn multiple_refund_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let index = NextFundIndex::::get(); - let account_id = Crowdloan::fund_account_id(index); - - // Set up a crowdloan ending on 9 - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 100000, 1, 1, 9, None)); - // Make more contributions than our limit - for i in 1..=RemoveKeysLimit::get() * 2 { - Balances::make_free_balance_be(&i.into(), (1000 * i).into()); - assert_ok!(Crowdloan::contribute( - RuntimeOrigin::signed(i.into()), - para, - (i * 100).into(), - None - )); - } - - assert_eq!(Balances::free_balance(account_id), 21000); - - // Move to the end of the crowdloan - run_to_block(10); - assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para)); - assert_eq!( - last_event(), - super::Event::::PartiallyRefunded { para_id: para }.into() - ); - - // Funds still left over - assert!(!Balances::free_balance(account_id).is_zero()); - - // Call again - assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para)); - assert_eq!(last_event(), super::Event::::AllRefunded { para_id: para }.into()); - - // Funds are returned - assert_eq!(Balances::free_balance(account_id), 0); - // 1 deposit for the crowdloan which hasn't dissolved yet. - for i in 1..=RemoveKeysLimit::get() * 2 { - assert_eq!(Balances::free_balance(&i.into()), i as u64 * 1000); - } - }); - } - - #[test] - fn refund_and_dissolve_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let issuance = Balances::total_issuance(); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); - - run_to_block(10); - // All funds are refunded - assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para)); - - // Now that `fund.raised` is zero, it can be dissolved. - assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); - assert_eq!(Balances::free_balance(1), 1000); - assert_eq!(Balances::free_balance(2), 2000); - assert_eq!(Balances::free_balance(3), 3000); - assert_eq!(Balances::total_issuance(), issuance); - }); - } - - // Regression test to check that a pot account with just one provider can be dissolved. - #[test] - fn dissolve_provider_refs_total_issuance_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let issuance = Balances::total_issuance(); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); - - run_to_block(10); - - // We test the historic case where crowdloan accounts only have one provider: - { - let fund = crowdloan::Funds::::get(para).unwrap(); - let pot = Crowdloan::fund_account_id(fund.fund_index); - System::dec_providers(&pot).unwrap(); - assert_eq!(System::providers(&pot), 1); - } - - // All funds are refunded - assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para)); - - // Now that `fund.raised` is zero, it can be dissolved. - assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); - - assert_eq!(Balances::free_balance(1), 1000); - assert_eq!(Balances::free_balance(2), 2000); - assert_eq!(Balances::free_balance(3), 3000); - assert_eq!(Balances::total_issuance(), issuance); - }); - } - - #[test] - fn dissolve_works() { - new_test_ext().execute_with(|| { - let para = new_para(); - let issuance = Balances::total_issuance(); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); - - // Can't dissolve before it ends - assert_noop!( - Crowdloan::dissolve(RuntimeOrigin::signed(1), para), - Error::::NotReadyToDissolve - ); - - run_to_block(10); - set_winner(para, 1, true); - // Can't dissolve when it won. - assert_noop!( - Crowdloan::dissolve(RuntimeOrigin::signed(1), para), - Error::::NotReadyToDissolve - ); - set_winner(para, 1, false); - - // Can't dissolve while it still has user funds - assert_noop!( - Crowdloan::dissolve(RuntimeOrigin::signed(1), para), - Error::::NotReadyToDissolve - ); - - // All funds are refunded - assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para)); - - // Now that `fund.raised` is zero, it can be dissolved. - assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); - assert_eq!(Balances::free_balance(1), 1000); - assert_eq!(Balances::free_balance(2), 2000); - assert_eq!(Balances::free_balance(3), 3000); - assert_eq!(Balances::total_issuance(), issuance); - }); - } - - #[test] - fn withdraw_from_finished_works() { - new_test_ext().execute_with(|| { - let ed: u64 = ::ExistentialDeposit::get(); - assert_eq!(ed, 1); - let para = new_para(); - let index = NextFundIndex::::get(); - let account_id = Crowdloan::fund_account_id(index); - - // Set up a crowdloan - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); - - // Fund crowdloans. - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); - // simulate the reserving of para's funds. this actually happens in the Slots pallet. - assert_ok!(Balances::reserve(&account_id, 149)); - - run_to_block(19); - assert_noop!( - Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para), - Error::::BidOrLeaseActive - ); - - run_to_block(20); - // simulate the unreserving of para's funds, now that the lease expired. this actually - // happens in the Slots pallet. - Balances::unreserve(&account_id, 150); - - // para has no reserved funds, indicating it did ot win the auction. - assert_eq!(Balances::reserved_balance(&account_id), 0); - // but there's still the funds in its balance. - assert_eq!(Balances::free_balance(&account_id), 150); - assert_eq!(Balances::free_balance(2), 1900); - assert_eq!(Balances::free_balance(3), 2950); - - assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para)); - assert_eq!(Balances::free_balance(&account_id), 50); - assert_eq!(Balances::free_balance(2), 2000); - - assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 3, para)); - assert_eq!(Balances::free_balance(&account_id), 0); - assert_eq!(Balances::free_balance(3), 3000); - }); - } - - #[test] - fn on_swap_works() { - new_test_ext().execute_with(|| { - let para_1 = new_para(); - let para_2 = new_para(); - - // Set up crowdloans - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_2, 1000, 1, 1, 9, None)); - // Different contributions - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para_2, 50, None)); - // Original state - assert_eq!(Funds::::get(para_1).unwrap().raised, 100); - assert_eq!(Funds::::get(para_2).unwrap().raised, 50); - // Swap - Crowdloan::on_swap(para_1, para_2); - // Final state - assert_eq!(Funds::::get(para_2).unwrap().raised, 100); - assert_eq!(Funds::::get(para_1).unwrap().raised, 50); - }); - } - - #[test] - fn cannot_create_fund_when_already_active() { - new_test_ext().execute_with(|| { - let para_1 = new_para(); - - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); - // Cannot create a fund again - assert_noop!( - Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None), - Error::::FundNotEnded, - ); - }); - } - - #[test] - fn edit_works() { - new_test_ext().execute_with(|| { - let para_1 = new_para(); +mod mock; - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None)); - let old_crowdloan = crowdloan::Funds::::get(para_1).unwrap(); - - assert_ok!(Crowdloan::edit(RuntimeOrigin::root(), para_1, 1234, 2, 3, 4, None)); - let new_crowdloan = crowdloan::Funds::::get(para_1).unwrap(); - - // Some things stay the same - assert_eq!(old_crowdloan.depositor, new_crowdloan.depositor); - assert_eq!(old_crowdloan.deposit, new_crowdloan.deposit); - assert_eq!(old_crowdloan.raised, new_crowdloan.raised); - - // Some things change - assert!(old_crowdloan.cap != new_crowdloan.cap); - assert!(old_crowdloan.first_period != new_crowdloan.first_period); - assert!(old_crowdloan.last_period != new_crowdloan.last_period); - }); - } - - #[test] - fn add_memo_works() { - new_test_ext().execute_with(|| { - let para_1 = new_para(); - - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); - // Cant add a memo before you have contributed. - assert_noop!( - Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, b"hello, world".to_vec()), - Error::::NoContributions, - ); - // Make a contribution. Initially no memo. - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para_1, 100, None)); - assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, vec![])); - // Can't place a memo that is too large. - assert_noop!( - Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, vec![123; 123]), - Error::::MemoTooLarge, - ); - // Adding a memo to an existing contribution works - assert_ok!(Crowdloan::add_memo( - RuntimeOrigin::signed(1), - para_1, - b"hello, world".to_vec() - )); - assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, b"hello, world".to_vec())); - // Can contribute again and data persists - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para_1, 100, None)); - assert_eq!(Crowdloan::contribution_get(0u32, &1), (200, b"hello, world".to_vec())); - }); - } - - #[test] - fn poke_works() { - new_test_ext().execute_with(|| { - let para_1 = new_para(); - - assert_ok!(TestAuctioneer::new_auction(5, 0)); - assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); - // Should fail when no contributions. - assert_noop!( - Crowdloan::poke(RuntimeOrigin::signed(1), para_1), - Error::::NoContributions - ); - assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None)); - run_to_block(6); - assert_ok!(Crowdloan::poke(RuntimeOrigin::signed(1), para_1)); - assert_eq!(crowdloan::NewRaise::::get(), vec![para_1]); - assert_noop!( - Crowdloan::poke(RuntimeOrigin::signed(1), para_1), - Error::::AlreadyInNewRaise - ); - }); - } -} +#[cfg(test)] +mod tests; #[cfg(feature = "runtime-benchmarks")] -mod benchmarking { - use super::{Pallet as Crowdloan, *}; - use frame_support::{assert_ok, traits::OnInitialize}; - use frame_system::RawOrigin; - use polkadot_runtime_parachains::paras; - use sp_core::crypto::UncheckedFrom; - use sp_runtime::traits::{Bounded, CheckedSub}; - - use frame_benchmarking::{account, benchmarks, whitelisted_caller}; - - fn assert_last_event(generic_event: ::RuntimeEvent) { - let events = frame_system::Pallet::::events(); - let system_event: ::RuntimeEvent = generic_event.into(); - // compare to the last event record - let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; - assert_eq!(event, &system_event); - } - - fn create_fund(id: u32, end: BlockNumberFor) -> ParaId { - let cap = BalanceOf::::max_value(); - let (_, offset) = T::Auctioneer::lease_period_length(); - // Set to the very beginning of lease period index 0. - frame_system::Pallet::::set_block_number(offset); - let now = frame_system::Pallet::::block_number(); - let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default(); - let first_period = lease_period_index; - let last_period = - lease_period_index + ((SlotRange::LEASE_PERIODS_PER_SLOT as u32) - 1).into(); - let para_id = id.into(); - - let caller = account("fund_creator", id, 0); - CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); - - // Assume ed25519 is most complex signature format - let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - - let head_data = T::Registrar::worst_head_data(); - let validation_code = T::Registrar::worst_validation_code(); - assert_ok!(T::Registrar::register( - caller.clone(), - para_id, - head_data, - validation_code.clone() - )); - assert_ok!(paras::Pallet::::add_trusted_validation_code( - frame_system::Origin::::Root.into(), - validation_code, - )); - T::Registrar::execute_pending_transitions(); - - assert_ok!(Crowdloan::::create( - RawOrigin::Signed(caller).into(), - para_id, - cap, - first_period, - last_period, - end, - Some(pubkey) - )); - - para_id - } - - fn contribute_fund(who: &T::AccountId, index: ParaId) { - CurrencyOf::::make_free_balance_be(&who, BalanceOf::::max_value()); - let value = T::MinContribution::get(); - - let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - let payload = (index, &who, BalanceOf::::default(), value); - let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey); - - assert_ok!(Crowdloan::::contribute( - RawOrigin::Signed(who.clone()).into(), - index, - value, - Some(sig) - )); - } - - benchmarks! { - where_clause { where T: paras::Config } - - create { - let para_id = ParaId::from(1_u32); - let cap = BalanceOf::::max_value(); - let first_period = 0u32.into(); - let last_period = 3u32.into(); - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - - let caller: T::AccountId = whitelisted_caller(); - let head_data = T::Registrar::worst_head_data(); - let validation_code = T::Registrar::worst_validation_code(); - - let verifier = MultiSigner::unchecked_from(account::<[u8; 32]>("verifier", 0, 0)); - - CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); - T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())?; - assert_ok!(paras::Pallet::::add_trusted_validation_code( - frame_system::Origin::::Root.into(), - validation_code, - )); - - T::Registrar::execute_pending_transitions(); - - }: _(RawOrigin::Signed(caller), para_id, cap, first_period, last_period, end, Some(verifier)) - verify { - assert_last_event::(Event::::Created { para_id }.into()) - } - - // Contribute has two arms: PreEnding and Ending, but both are equal complexity. - contribute { - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - let fund_index = create_fund::(1, end); - let caller: T::AccountId = whitelisted_caller(); - let contribution = T::MinContribution::get(); - CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); - assert!(NewRaise::::get().is_empty()); - - let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - let payload = (fund_index, &caller, BalanceOf::::default(), contribution); - let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey); - - }: _(RawOrigin::Signed(caller.clone()), fund_index, contribution, Some(sig)) - verify { - // NewRaise is appended to, so we don't need to fill it up for worst case scenario. - assert!(!NewRaise::::get().is_empty()); - assert_last_event::(Event::::Contributed { who: caller, fund_index, amount: contribution }.into()); - } - - withdraw { - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - let fund_index = create_fund::(1337, end); - let caller: T::AccountId = whitelisted_caller(); - let contributor = account("contributor", 0, 0); - contribute_fund::(&contributor, fund_index); - frame_system::Pallet::::set_block_number(BlockNumberFor::::max_value()); - }: _(RawOrigin::Signed(caller), contributor.clone(), fund_index) - verify { - assert_last_event::(Event::::Withdrew { who: contributor, fund_index, amount: T::MinContribution::get() }.into()); - } - - // Worst case: Refund removes `RemoveKeysLimit` keys, and is fully refunded. - #[skip_meta] - refund { - let k in 0 .. T::RemoveKeysLimit::get(); - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - let fund_index = create_fund::(1337, end); - - // Dissolve will remove at most `RemoveKeysLimit` at once. - for i in 0 .. k { - contribute_fund::(&account("contributor", i, 0), fund_index); - } - - let caller: T::AccountId = whitelisted_caller(); - frame_system::Pallet::::set_block_number(BlockNumberFor::::max_value()); - }: _(RawOrigin::Signed(caller), fund_index) - verify { - assert_last_event::(Event::::AllRefunded { para_id: fund_index }.into()); - } - - dissolve { - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - let fund_index = create_fund::(1337, end); - let caller: T::AccountId = whitelisted_caller(); - frame_system::Pallet::::set_block_number(BlockNumberFor::::max_value()); - }: _(RawOrigin::Signed(caller.clone()), fund_index) - verify { - assert_last_event::(Event::::Dissolved { para_id: fund_index }.into()); - } - - edit { - let para_id = ParaId::from(1_u32); - let cap = BalanceOf::::max_value(); - let first_period = 0u32.into(); - let last_period = 3u32.into(); - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - - let caller: T::AccountId = whitelisted_caller(); - let head_data = T::Registrar::worst_head_data(); - let validation_code = T::Registrar::worst_validation_code(); - - let verifier = MultiSigner::unchecked_from(account::<[u8; 32]>("verifier", 0, 0)); - - CurrencyOf::::make_free_balance_be(&caller, BalanceOf::::max_value()); - T::Registrar::register(caller.clone(), para_id, head_data, validation_code.clone())?; - assert_ok!(paras::Pallet::::add_trusted_validation_code( - frame_system::Origin::::Root.into(), - validation_code, - )); - - T::Registrar::execute_pending_transitions(); - - Crowdloan::::create( - RawOrigin::Signed(caller).into(), - para_id, cap, first_period, last_period, end, Some(verifier.clone()), - )?; - - // Doesn't matter what we edit to, so use the same values. - }: _(RawOrigin::Root, para_id, cap, first_period, last_period, end, Some(verifier)) - verify { - assert_last_event::(Event::::Edited { para_id }.into()) - } - - add_memo { - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - let fund_index = create_fund::(1, end); - let caller: T::AccountId = whitelisted_caller(); - contribute_fund::(&caller, fund_index); - let worst_memo = vec![42; T::MaxMemoLength::get().into()]; - }: _(RawOrigin::Signed(caller.clone()), fund_index, worst_memo.clone()) - verify { - let fund = Funds::::get(fund_index).expect("fund was created..."); - assert_eq!( - Crowdloan::::contribution_get(fund.fund_index, &caller), - (T::MinContribution::get(), worst_memo), - ); - } - - poke { - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end = lpl + offset; - let fund_index = create_fund::(1, end); - let caller: T::AccountId = whitelisted_caller(); - contribute_fund::(&caller, fund_index); - NewRaise::::kill(); - assert!(NewRaise::::get().is_empty()); - }: _(RawOrigin::Signed(caller), fund_index) - verify { - assert!(!NewRaise::::get().is_empty()); - assert_last_event::(Event::::AddedToNewRaise { para_id: fund_index }.into()) - } - - // Worst case scenario: N funds are all in the `NewRaise` list, we are - // in the beginning of the ending period, and each fund outbids the next - // over the same periods. - on_initialize { - // We test the complexity over different number of new raise - let n in 2 .. 100; - let (lpl, offset) = T::Auctioneer::lease_period_length(); - let end_block = lpl + offset - 1u32.into(); - - let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); - - for i in 0 .. n { - let fund_index = create_fund::(i, end_block); - let contributor: T::AccountId = account("contributor", i, 0); - let contribution = T::MinContribution::get() * (i + 1).into(); - let payload = (fund_index, &contributor, BalanceOf::::default(), contribution); - let sig = crypto::create_ed25519_signature(&payload.encode(), pubkey.clone()); - - CurrencyOf::::make_free_balance_be(&contributor, BalanceOf::::max_value()); - Crowdloan::::contribute(RawOrigin::Signed(contributor).into(), fund_index, contribution, Some(sig))?; - } - - let now = frame_system::Pallet::::block_number(); - let (lease_period_index, _) = T::Auctioneer::lease_period_index(now).unwrap_or_default(); - let duration = end_block - .checked_sub(&frame_system::Pallet::::block_number()) - .ok_or("duration of auction less than zero")?; - T::Auctioneer::new_auction(duration, lease_period_index)?; - - assert_eq!(T::Auctioneer::auction_status(end_block).is_ending(), Some((0u32.into(), 0u32.into()))); - assert_eq!(NewRaise::::get().len(), n as usize); - let old_endings_count = EndingsCount::::get(); - }: { - Crowdloan::::on_initialize(end_block); - } verify { - assert_eq!(EndingsCount::::get(), old_endings_count + 1); - assert_last_event::(Event::::HandleBidResult { para_id: (n - 1).into(), result: Ok(()) }.into()); - } - - impl_benchmark_test_suite!( - Crowdloan, - crate::integration_tests::new_test_ext_with_offset(10), - crate::integration_tests::Test, - ); - } -} +mod benchmarking; diff --git a/polkadot/runtime/common/src/crowdloan/tests.rs b/polkadot/runtime/common/src/crowdloan/tests.rs new file mode 100644 index 000000000000..3837f592851c --- /dev/null +++ b/polkadot/runtime/common/src/crowdloan/tests.rs @@ -0,0 +1,846 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Tests for the crowdloan pallet. + +#[cfg(test)] +use super::*; + +use crate::crowdloan::mock::*; +use frame_support::{assert_noop, assert_ok}; +use polkadot_primitives::Id as ParaId; +// The testing primitives are very useful for avoiding having to work with signatures +// or public keys. `u64` is used as the `AccountId` and no `Signature`s are required. +use crate::{ + crowdloan, + mock::TestRegistrar, + traits::{AuctionStatus, OnSwap}, +}; +use pallet_balances::Error as BalancesError; +use polkadot_primitives_test_helpers::{dummy_head_data, dummy_validation_code}; +use sp_runtime::traits::TrailingZeroInput; + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with(|| { + assert_eq!(System::block_number(), 0); + assert_eq!(crowdloan::Funds::::get(ParaId::from(0)), None); + let empty: Vec = Vec::new(); + assert_eq!(crowdloan::NewRaise::::get(), empty); + assert_eq!(Crowdloan::contribution_get(0u32, &1).0, 0); + assert_eq!(crowdloan::EndingsCount::::get(), 0); + + assert_ok!(TestAuctioneer::new_auction(5, 0)); + + assert_eq!(bids(), vec![]); + assert_ok!(TestAuctioneer::place_bid(1, 2.into(), 0, 3, 6)); + let b = BidPlaced { + height: 0, + bidder: 1, + para: 2.into(), + first_period: 0, + last_period: 3, + amount: 6, + }; + assert_eq!(bids(), vec![b]); + assert_eq!(TestAuctioneer::auction_status(4), AuctionStatus::::StartingPeriod); + assert_eq!(TestAuctioneer::auction_status(5), AuctionStatus::::EndingPeriod(0, 0)); + assert_eq!(TestAuctioneer::auction_status(9), AuctionStatus::::EndingPeriod(4, 0)); + assert_eq!(TestAuctioneer::auction_status(11), AuctionStatus::::NotStarted); + }); +} + +#[test] +fn create_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + // Now try to create a crowdloan campaign + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None)); + // This is what the initial `fund_info` should look like + let fund_info = FundInfo { + depositor: 1, + verifier: None, + deposit: 1, + raised: 0, + // 5 blocks length + 3 block ending period + 1 starting block + end: 9, + cap: 1000, + last_contribution: LastContribution::Never, + first_period: 1, + last_period: 4, + fund_index: 0, + }; + assert_eq!(crowdloan::Funds::::get(para), Some(fund_info)); + // User has deposit removed from their free balance + assert_eq!(Balances::free_balance(1), 999); + // Deposit is placed in reserved + assert_eq!(Balances::reserved_balance(1), 1); + // No new raise until first contribution + let empty: Vec = Vec::new(); + assert_eq!(crowdloan::NewRaise::::get(), empty); + }); +} + +#[test] +fn create_with_verifier_works() { + new_test_ext().execute_with(|| { + let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + let para = new_para(); + // Now try to create a crowdloan campaign + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(1), + para, + 1000, + 1, + 4, + 9, + Some(pubkey.clone()) + )); + // This is what the initial `fund_info` should look like + let fund_info = FundInfo { + depositor: 1, + verifier: Some(pubkey), + deposit: 1, + raised: 0, + // 5 blocks length + 3 block ending period + 1 starting block + end: 9, + cap: 1000, + last_contribution: LastContribution::Never, + first_period: 1, + last_period: 4, + fund_index: 0, + }; + assert_eq!(crowdloan::Funds::::get(ParaId::from(0)), Some(fund_info)); + // User has deposit removed from their free balance + assert_eq!(Balances::free_balance(1), 999); + // Deposit is placed in reserved + assert_eq!(Balances::reserved_balance(1), 1); + // No new raise until first contribution + let empty: Vec = Vec::new(); + assert_eq!(crowdloan::NewRaise::::get(), empty); + }); +} + +#[test] +fn create_handles_basic_errors() { + new_test_ext().execute_with(|| { + // Now try to create a crowdloan campaign + let para = new_para(); + + let e = Error::::InvalidParaId; + assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), 1.into(), 1000, 1, 4, 9, None), e); + // Cannot create a crowdloan with bad lease periods + let e = Error::::LastPeriodBeforeFirstPeriod; + assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 4, 1, 9, None), e); + let e = Error::::LastPeriodTooFarInFuture; + assert_noop!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 9, 9, None), e); + + // Cannot create a crowdloan without some deposit funds + assert_ok!(TestRegistrar::::register( + 1337, + ParaId::from(1234), + dummy_head_data(), + dummy_validation_code() + )); + let e = BalancesError::::InsufficientBalance; + assert_noop!( + Crowdloan::create(RuntimeOrigin::signed(1337), ParaId::from(1234), 1000, 1, 3, 9, None), + e + ); + + // Cannot create a crowdloan with nonsense end date + // This crowdloan would end in lease period 2, but is bidding for some slot that starts + // in lease period 1. + assert_noop!( + Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 41, None), + Error::::EndTooFarInFuture + ); + }); +} + +#[test] +fn contribute_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None)); + + // No contributions yet + assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0); + + // User 1 contributes to their own crowdloan + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None)); + // User 1 has spent some funds to do this, transfer fees **are** taken + assert_eq!(Balances::free_balance(1), 950); + // Contributions are stored in the trie + assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 49); + // Contributions appear in free balance of crowdloan + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(index)), 49); + // Crowdloan is added to NewRaise + assert_eq!(crowdloan::NewRaise::::get(), vec![para]); + + let fund = crowdloan::Funds::::get(para).unwrap(); + + // Last contribution time recorded + assert_eq!(fund.last_contribution, LastContribution::PreEnding(0)); + assert_eq!(fund.raised, 49); + }); +} + +#[test] +fn contribute_with_verifier_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec()); + // Set up a crowdloan + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(1), + para, + 1000, + 1, + 4, + 9, + Some(pubkey.clone()) + )); + + // No contributions yet + assert_eq!(Crowdloan::contribution_get(u32::from(para), &1).0, 0); + + // Missing signature + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None), + Error::::InvalidSignature + ); + + let payload = (0u32, 1u64, 0u64, 49u64); + let valid_signature = crypto::create_ed25519_signature(&payload.encode(), pubkey.clone()); + let invalid_signature = MultiSignature::decode(&mut TrailingZeroInput::zeroes()).unwrap(); + + // Invalid signature + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, Some(invalid_signature)), + Error::::InvalidSignature + ); + + // Valid signature wrong parameter + assert_noop!( + Crowdloan::contribute( + RuntimeOrigin::signed(1), + para, + 50, + Some(valid_signature.clone()) + ), + Error::::InvalidSignature + ); + assert_noop!( + Crowdloan::contribute( + RuntimeOrigin::signed(2), + para, + 49, + Some(valid_signature.clone()) + ), + Error::::InvalidSignature + ); + + // Valid signature + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(1), + para, + 49, + Some(valid_signature.clone()) + )); + + // Reuse valid signature + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, Some(valid_signature)), + Error::::InvalidSignature + ); + + let payload_2 = (0u32, 1u64, 49u64, 10u64); + let valid_signature_2 = crypto::create_ed25519_signature(&payload_2.encode(), pubkey); + + // New valid signature + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(1), + para, + 10, + Some(valid_signature_2) + )); + + // Contributions appear in free balance of crowdloan + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(index)), 59); + + // Contribution amount is correct + let fund = crowdloan::Funds::::get(para).unwrap(); + assert_eq!(fund.raised, 59); + }); +} + +#[test] +fn contribute_handles_basic_errors() { + new_test_ext().execute_with(|| { + let para = new_para(); + + // Cannot contribute to non-existing fund + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None), + Error::::InvalidParaId + ); + // Cannot contribute below minimum contribution + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para, 9, None), + Error::::ContributionTooSmall + ); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 4, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 101, None)); + + // Cannot contribute past the limit + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(2), para, 900, None), + Error::::CapExceeded + ); + + // Move past end date + run_to_block(10); + + // Cannot contribute to ended fund + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para, 49, None), + Error::::ContributionPeriodOver + ); + + // If a crowdloan has already won, it should not allow contributions. + let para_2 = new_para(); + let index = NextFundIndex::::get(); + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_2, 1000, 1, 4, 40, None)); + // Emulate a win by leasing out and putting a deposit. Slots pallet would normally do + // this. + let crowdloan_account = Crowdloan::fund_account_id(index); + set_winner(para_2, crowdloan_account, true); + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para_2, 49, None), + Error::::BidOrLeaseActive + ); + + // Move past lease period 1, should not be allowed to have further contributions with a + // crowdloan that has starting period 1. + let para_3 = new_para(); + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_3, 1000, 1, 4, 40, None)); + run_to_block(40); + let now = System::block_number(); + assert_eq!(TestAuctioneer::lease_period_index(now).unwrap().0, 2); + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(1), para_3, 49, None), + Error::::ContributionPeriodOver + ); + }); +} + +#[test] +fn cannot_contribute_during_vrf() { + new_test_ext().execute_with(|| { + set_vrf_delay(5); + + let para = new_para(); + let first_period = 1; + let last_period = 4; + + assert_ok!(TestAuctioneer::new_auction(5, 0)); + + // Set up a crowdloan + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(1), + para, + 1000, + first_period, + last_period, + 20, + None + )); + + run_to_block(8); + // Can def contribute when auction is running. + assert!(TestAuctioneer::auction_status(System::block_number()).is_ending().is_some()); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None)); + + run_to_block(10); + // Can't contribute when auction is in the VRF delay period. + assert!(TestAuctioneer::auction_status(System::block_number()).is_vrf()); + assert_noop!( + Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None), + Error::::VrfDelayInProgress + ); + + run_to_block(15); + // Its fine to contribute when no auction is running. + assert!(!TestAuctioneer::auction_status(System::block_number()).is_in_progress()); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None)); + }) +} + +#[test] +fn bidding_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + let first_period = 1; + let last_period = 4; + + assert_ok!(TestAuctioneer::new_auction(5, 0)); + + // Set up a crowdloan + assert_ok!(Crowdloan::create( + RuntimeOrigin::signed(1), + para, + 1000, + first_period, + last_period, + 9, + None + )); + let bidder = Crowdloan::fund_account_id(index); + + // Fund crowdloan + run_to_block(1); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + run_to_block(3); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 150, None)); + run_to_block(5); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(4), para, 200, None)); + run_to_block(8); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 250, None)); + run_to_block(10); + + assert_eq!( + bids(), + vec![ + BidPlaced { height: 5, amount: 250, bidder, para, first_period, last_period }, + BidPlaced { height: 6, amount: 450, bidder, para, first_period, last_period }, + BidPlaced { height: 9, amount: 700, bidder, para, first_period, last_period }, + ] + ); + + // Endings count incremented + assert_eq!(crowdloan::EndingsCount::::get(), 1); + }); +} + +#[test] +fn withdraw_from_failed_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); + + run_to_block(10); + let account_id = Crowdloan::fund_account_id(index); + // para has no reserved funds, indicating it did not win the auction. + assert_eq!(Balances::reserved_balance(&account_id), 0); + // but there's still the funds in its balance. + assert_eq!(Balances::free_balance(&account_id), 150); + assert_eq!(Balances::free_balance(2), 1900); + assert_eq!(Balances::free_balance(3), 2950); + + assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para)); + assert_eq!(Balances::free_balance(&account_id), 50); + assert_eq!(Balances::free_balance(2), 2000); + + assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 3, para)); + assert_eq!(Balances::free_balance(&account_id), 0); + assert_eq!(Balances::free_balance(3), 3000); + }); +} + +#[test] +fn withdraw_cannot_be_griefed() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + let issuance = Balances::total_issuance(); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + + run_to_block(10); + let account_id = Crowdloan::fund_account_id(index); + + // user sends the crowdloan funds trying to make an accounting error + assert_ok!(Balances::transfer_allow_death(RuntimeOrigin::signed(1), account_id, 10)); + + // overfunded now + assert_eq!(Balances::free_balance(&account_id), 110); + assert_eq!(Balances::free_balance(2), 1900); + + assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para)); + assert_eq!(Balances::free_balance(2), 2000); + + // Some funds are left over + assert_eq!(Balances::free_balance(&account_id), 10); + // Remaining funds will be burned + assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); + assert_eq!(Balances::free_balance(&account_id), 0); + assert_eq!(Balances::total_issuance(), issuance - 10); + }); +} + +#[test] +fn refund_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + let account_id = Crowdloan::fund_account_id(index); + + // Set up a crowdloan ending on 9 + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + // Make some contributions + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 200, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 300, None)); + + assert_eq!(Balances::free_balance(account_id), 600); + + // Can't refund before the crowdloan it has ended + assert_noop!( + Crowdloan::refund(RuntimeOrigin::signed(1337), para), + Error::::FundNotEnded, + ); + + // Move to the end of the crowdloan + run_to_block(10); + assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para)); + + // Funds are returned + assert_eq!(Balances::free_balance(account_id), 0); + // 1 deposit for the crowdloan which hasn't dissolved yet. + assert_eq!(Balances::free_balance(1), 1000 - 1); + assert_eq!(Balances::free_balance(2), 2000); + assert_eq!(Balances::free_balance(3), 3000); + }); +} + +#[test] +fn multiple_refund_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let index = NextFundIndex::::get(); + let account_id = Crowdloan::fund_account_id(index); + + // Set up a crowdloan ending on 9 + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 100000, 1, 1, 9, None)); + // Make more contributions than our limit + for i in 1..=RemoveKeysLimit::get() * 2 { + Balances::make_free_balance_be(&i.into(), (1000 * i).into()); + assert_ok!(Crowdloan::contribute( + RuntimeOrigin::signed(i.into()), + para, + (i * 100).into(), + None + )); + } + + assert_eq!(Balances::free_balance(account_id), 21000); + + // Move to the end of the crowdloan + run_to_block(10); + assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para)); + assert_eq!(last_event(), super::Event::::PartiallyRefunded { para_id: para }.into()); + + // Funds still left over + assert!(!Balances::free_balance(account_id).is_zero()); + + // Call again + assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(1337), para)); + assert_eq!(last_event(), super::Event::::AllRefunded { para_id: para }.into()); + + // Funds are returned + assert_eq!(Balances::free_balance(account_id), 0); + // 1 deposit for the crowdloan which hasn't dissolved yet. + for i in 1..=RemoveKeysLimit::get() * 2 { + assert_eq!(Balances::free_balance(&i.into()), i as u64 * 1000); + } + }); +} + +#[test] +fn refund_and_dissolve_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let issuance = Balances::total_issuance(); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); + + run_to_block(10); + // All funds are refunded + assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para)); + + // Now that `fund.raised` is zero, it can be dissolved. + assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); + assert_eq!(Balances::free_balance(1), 1000); + assert_eq!(Balances::free_balance(2), 2000); + assert_eq!(Balances::free_balance(3), 3000); + assert_eq!(Balances::total_issuance(), issuance); + }); +} + +// Regression test to check that a pot account with just one provider can be dissolved. +#[test] +fn dissolve_provider_refs_total_issuance_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let issuance = Balances::total_issuance(); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); + + run_to_block(10); + + // We test the historic case where crowdloan accounts only have one provider: + { + let fund = crowdloan::Funds::::get(para).unwrap(); + let pot = Crowdloan::fund_account_id(fund.fund_index); + System::dec_providers(&pot).unwrap(); + assert_eq!(System::providers(&pot), 1); + } + + // All funds are refunded + assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para)); + + // Now that `fund.raised` is zero, it can be dissolved. + assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); + + assert_eq!(Balances::free_balance(1), 1000); + assert_eq!(Balances::free_balance(2), 2000); + assert_eq!(Balances::free_balance(3), 3000); + assert_eq!(Balances::total_issuance(), issuance); + }); +} + +#[test] +fn dissolve_works() { + new_test_ext().execute_with(|| { + let para = new_para(); + let issuance = Balances::total_issuance(); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); + + // Can't dissolve before it ends + assert_noop!( + Crowdloan::dissolve(RuntimeOrigin::signed(1), para), + Error::::NotReadyToDissolve + ); + + run_to_block(10); + set_winner(para, 1, true); + // Can't dissolve when it won. + assert_noop!( + Crowdloan::dissolve(RuntimeOrigin::signed(1), para), + Error::::NotReadyToDissolve + ); + set_winner(para, 1, false); + + // Can't dissolve while it still has user funds + assert_noop!( + Crowdloan::dissolve(RuntimeOrigin::signed(1), para), + Error::::NotReadyToDissolve + ); + + // All funds are refunded + assert_ok!(Crowdloan::refund(RuntimeOrigin::signed(2), para)); + + // Now that `fund.raised` is zero, it can be dissolved. + assert_ok!(Crowdloan::dissolve(RuntimeOrigin::signed(1), para)); + assert_eq!(Balances::free_balance(1), 1000); + assert_eq!(Balances::free_balance(2), 2000); + assert_eq!(Balances::free_balance(3), 3000); + assert_eq!(Balances::total_issuance(), issuance); + }); +} + +#[test] +fn withdraw_from_finished_works() { + new_test_ext().execute_with(|| { + let ed: u64 = ::ExistentialDeposit::get(); + assert_eq!(ed, 1); + let para = new_para(); + let index = NextFundIndex::::get(); + let account_id = Crowdloan::fund_account_id(index); + + // Set up a crowdloan + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para, 1000, 1, 1, 9, None)); + + // Fund crowdloans. + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para, 50, None)); + // simulate the reserving of para's funds. this actually happens in the Slots pallet. + assert_ok!(Balances::reserve(&account_id, 149)); + + run_to_block(19); + assert_noop!( + Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para), + Error::::BidOrLeaseActive + ); + + run_to_block(20); + // simulate the unreserving of para's funds, now that the lease expired. this actually + // happens in the Slots pallet. + Balances::unreserve(&account_id, 150); + + // para has no reserved funds, indicating it did ot win the auction. + assert_eq!(Balances::reserved_balance(&account_id), 0); + // but there's still the funds in its balance. + assert_eq!(Balances::free_balance(&account_id), 150); + assert_eq!(Balances::free_balance(2), 1900); + assert_eq!(Balances::free_balance(3), 2950); + + assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 2, para)); + assert_eq!(Balances::free_balance(&account_id), 50); + assert_eq!(Balances::free_balance(2), 2000); + + assert_ok!(Crowdloan::withdraw(RuntimeOrigin::signed(2), 3, para)); + assert_eq!(Balances::free_balance(&account_id), 0); + assert_eq!(Balances::free_balance(3), 3000); + }); +} + +#[test] +fn on_swap_works() { + new_test_ext().execute_with(|| { + let para_1 = new_para(); + let para_2 = new_para(); + + // Set up crowdloans + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_2, 1000, 1, 1, 9, None)); + // Different contributions + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(3), para_2, 50, None)); + // Original state + assert_eq!(Funds::::get(para_1).unwrap().raised, 100); + assert_eq!(Funds::::get(para_2).unwrap().raised, 50); + // Swap + Crowdloan::on_swap(para_1, para_2); + // Final state + assert_eq!(Funds::::get(para_2).unwrap().raised, 100); + assert_eq!(Funds::::get(para_1).unwrap().raised, 50); + }); +} + +#[test] +fn cannot_create_fund_when_already_active() { + new_test_ext().execute_with(|| { + let para_1 = new_para(); + + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); + // Cannot create a fund again + assert_noop!( + Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None), + Error::::FundNotEnded, + ); + }); +} + +#[test] +fn edit_works() { + new_test_ext().execute_with(|| { + let para_1 = new_para(); + + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None)); + let old_crowdloan = crowdloan::Funds::::get(para_1).unwrap(); + + assert_ok!(Crowdloan::edit(RuntimeOrigin::root(), para_1, 1234, 2, 3, 4, None)); + let new_crowdloan = crowdloan::Funds::::get(para_1).unwrap(); + + // Some things stay the same + assert_eq!(old_crowdloan.depositor, new_crowdloan.depositor); + assert_eq!(old_crowdloan.deposit, new_crowdloan.deposit); + assert_eq!(old_crowdloan.raised, new_crowdloan.raised); + + // Some things change + assert!(old_crowdloan.cap != new_crowdloan.cap); + assert!(old_crowdloan.first_period != new_crowdloan.first_period); + assert!(old_crowdloan.last_period != new_crowdloan.last_period); + }); +} + +#[test] +fn add_memo_works() { + new_test_ext().execute_with(|| { + let para_1 = new_para(); + + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); + // Cant add a memo before you have contributed. + assert_noop!( + Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, b"hello, world".to_vec()), + Error::::NoContributions, + ); + // Make a contribution. Initially no memo. + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para_1, 100, None)); + assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, vec![])); + // Can't place a memo that is too large. + assert_noop!( + Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, vec![123; 123]), + Error::::MemoTooLarge, + ); + // Adding a memo to an existing contribution works + assert_ok!(Crowdloan::add_memo(RuntimeOrigin::signed(1), para_1, b"hello, world".to_vec())); + assert_eq!(Crowdloan::contribution_get(0u32, &1), (100, b"hello, world".to_vec())); + // Can contribute again and data persists + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(1), para_1, 100, None)); + assert_eq!(Crowdloan::contribution_get(0u32, &1), (200, b"hello, world".to_vec())); + }); +} + +#[test] +fn poke_works() { + new_test_ext().execute_with(|| { + let para_1 = new_para(); + + assert_ok!(TestAuctioneer::new_auction(5, 0)); + assert_ok!(Crowdloan::create(RuntimeOrigin::signed(1), para_1, 1000, 1, 1, 9, None)); + // Should fail when no contributions. + assert_noop!( + Crowdloan::poke(RuntimeOrigin::signed(1), para_1), + Error::::NoContributions + ); + assert_ok!(Crowdloan::contribute(RuntimeOrigin::signed(2), para_1, 100, None)); + run_to_block(6); + assert_ok!(Crowdloan::poke(RuntimeOrigin::signed(1), para_1)); + assert_eq!(crowdloan::NewRaise::::get(), vec![para_1]); + assert_noop!( + Crowdloan::poke(RuntimeOrigin::signed(1), para_1), + Error::::AlreadyInNewRaise + ); + }); +} From 2e96ccdcfdf74aa4389b73029a0147a66b87d591 Mon Sep 17 00:00:00 2001 From: Krayt78 Date: Sat, 7 Dec 2024 16:53:10 +0100 Subject: [PATCH 2/2] extracted mod data to new benchmarking/mock/tests files --- .../runtime/common/src/slots/benchmarking.rs | 198 ++++++ polkadot/runtime/common/src/slots/mock.rs | 113 +++ polkadot/runtime/common/src/slots/mod.rs | 650 +----------------- polkadot/runtime/common/src/slots/tests.rs | 392 +++++++++++ 4 files changed, 707 insertions(+), 646 deletions(-) create mode 100644 polkadot/runtime/common/src/slots/benchmarking.rs create mode 100644 polkadot/runtime/common/src/slots/mock.rs create mode 100644 polkadot/runtime/common/src/slots/tests.rs diff --git a/polkadot/runtime/common/src/slots/benchmarking.rs b/polkadot/runtime/common/src/slots/benchmarking.rs new file mode 100644 index 000000000000..3d1eb7c67812 --- /dev/null +++ b/polkadot/runtime/common/src/slots/benchmarking.rs @@ -0,0 +1,198 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Benchmarking for slots pallet + +#[cfg(feature = "runtime-benchmarks")] +use super::*; +use frame_support::assert_ok; +use frame_system::RawOrigin; +use polkadot_runtime_parachains::paras; +use sp_runtime::traits::{Bounded, One}; + +use frame_benchmarking::{account, benchmarks, whitelisted_caller, BenchmarkError}; + +use crate::slots::Pallet as Slots; + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +// Registers a parathread (on-demand parachain) +fn register_a_parathread(i: u32) -> (ParaId, T::AccountId) { + let para = ParaId::from(i); + let leaser: T::AccountId = account("leaser", i, 0); + T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); + let worst_head_data = T::Registrar::worst_head_data(); + let worst_validation_code = T::Registrar::worst_validation_code(); + + assert_ok!(T::Registrar::register( + leaser.clone(), + para, + worst_head_data, + worst_validation_code.clone(), + )); + assert_ok!(paras::Pallet::::add_trusted_validation_code( + frame_system::Origin::::Root.into(), + worst_validation_code, + )); + + T::Registrar::execute_pending_transitions(); + + (para, leaser) +} + +benchmarks! { + where_clause { where T: paras::Config } + + force_lease { + // If there is an offset, we need to be on that block to be able to do lease things. + frame_system::Pallet::::set_block_number(T::LeaseOffset::get() + One::one()); + let para = ParaId::from(1337); + let leaser: T::AccountId = account("leaser", 0, 0); + T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); + let amount = T::Currency::minimum_balance(); + let period_begin = 69u32.into(); + let period_count = 3u32.into(); + let origin = + T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(origin, para, leaser.clone(), amount, period_begin, period_count) + verify { + assert_last_event::(Event::::Leased { + para_id: para, + leaser, period_begin, + period_count, + extra_reserved: amount, + total_amount: amount, + }.into()); + } + + // Worst case scenario, T on-demand parachains onboard, and C lease holding parachains offboard. + manage_lease_period_start { + // Assume reasonable maximum of 100 paras at any time + let c in 0 .. 100; + let t in 0 .. 100; + + let period_begin = 1u32.into(); + let period_count = 4u32.into(); + + // If there is an offset, we need to be on that block to be able to do lease things. + frame_system::Pallet::::set_block_number(T::LeaseOffset::get() + One::one()); + + // Make T parathreads (on-demand parachains) + let paras_info = (0..t).map(|i| { + register_a_parathread::(i) + }).collect::>(); + + T::Registrar::execute_pending_transitions(); + + // T on-demand parachains are upgrading to lease holding parachains + for (para, leaser) in paras_info { + let amount = T::Currency::minimum_balance(); + let origin = T::ForceOrigin::try_successful_origin() + .expect("ForceOrigin has no successful origin required for the benchmark"); + Slots::::force_lease(origin, para, leaser, amount, period_begin, period_count)?; + } + + T::Registrar::execute_pending_transitions(); + + // C lease holding parachains are downgrading to on-demand parachains + for i in 200 .. 200 + c { + let (para, leaser) = register_a_parathread::(i); + T::Registrar::make_parachain(para)?; + } + + T::Registrar::execute_pending_transitions(); + + for i in 0 .. t { + assert!(T::Registrar::is_parathread(ParaId::from(i))); + } + + for i in 200 .. 200 + c { + assert!(T::Registrar::is_parachain(ParaId::from(i))); + } + }: { + Slots::::manage_lease_period_start(period_begin); + } verify { + // All paras should have switched. + T::Registrar::execute_pending_transitions(); + for i in 0 .. t { + assert!(T::Registrar::is_parachain(ParaId::from(i))); + } + for i in 200 .. 200 + c { + assert!(T::Registrar::is_parathread(ParaId::from(i))); + } + } + + // Assume that at most 8 people have deposits for leases on a parachain. + // This would cover at least 4 years of leases in the worst case scenario. + clear_all_leases { + let max_people = 8; + let (para, _) = register_a_parathread::(1); + + // If there is an offset, we need to be on that block to be able to do lease things. + frame_system::Pallet::::set_block_number(T::LeaseOffset::get() + One::one()); + + for i in 0 .. max_people { + let leaser = account("lease_deposit", i, 0); + let amount = T::Currency::minimum_balance(); + T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); + + // Average slot has 4 lease periods. + let period_count: LeasePeriodOf = 4u32.into(); + let period_begin = period_count * i.into(); + let origin = T::ForceOrigin::try_successful_origin() + .expect("ForceOrigin has no successful origin required for the benchmark"); + Slots::::force_lease(origin, para, leaser, amount, period_begin, period_count)?; + } + + for i in 0 .. max_people { + let leaser = account("lease_deposit", i, 0); + assert_eq!(T::Currency::reserved_balance(&leaser), T::Currency::minimum_balance()); + } + + let origin = + T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; + }: _(origin, para) + verify { + for i in 0 .. max_people { + let leaser = account("lease_deposit", i, 0); + assert_eq!(T::Currency::reserved_balance(&leaser), 0u32.into()); + } + } + + trigger_onboard { + // get a parachain into a bad state where they did not onboard + let (para, _) = register_a_parathread::(1); + Leases::::insert(para, vec![Some((account::("lease_insert", 0, 0), BalanceOf::::default()))]); + assert!(T::Registrar::is_parathread(para)); + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), para) + verify { + T::Registrar::execute_pending_transitions(); + assert!(T::Registrar::is_parachain(para)); + } + + impl_benchmark_test_suite!( + Slots, + crate::integration_tests::new_test_ext(), + crate::integration_tests::Test, + ); +} diff --git a/polkadot/runtime/common/src/slots/mock.rs b/polkadot/runtime/common/src/slots/mock.rs new file mode 100644 index 000000000000..b57792c24b29 --- /dev/null +++ b/polkadot/runtime/common/src/slots/mock.rs @@ -0,0 +1,113 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Mocking utilities for testing in slots pallet. + +#[cfg(test)] +use super::*; + +use crate::{mock::TestRegistrar, slots}; +use frame_support::{derive_impl, parameter_types}; +use frame_system::EnsureRoot; +use pallet_balances; +use polkadot_primitives::BlockNumber; +use sp_core::H256; +use sp_runtime::{ + traits::{BlakeTwo256, IdentityLookup}, + BuildStorage, +}; + +type Block = frame_system::mocking::MockBlockU32; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Slots: slots, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +parameter_types! { + pub const LeasePeriod: BlockNumber = 10; + pub static LeaseOffset: BlockNumber = 0; + pub const ParaDeposit: u64 = 1; +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type Registrar = TestRegistrar; + type LeasePeriod = LeasePeriod; + type LeaseOffset = LeaseOffset; + type ForceOrigin = EnsureRoot; + type WeightInfo = crate::slots::TestWeightInfo; +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mock up. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], + } + .assimilate_storage(&mut t) + .unwrap(); + t.into() +} + +pub fn run_to_block(n: BlockNumber) { + while System::block_number() < n { + Slots::on_finalize(System::block_number()); + Balances::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()); + Balances::on_initialize(System::block_number()); + Slots::on_initialize(System::block_number()); + } +} diff --git a/polkadot/runtime/common/src/slots/mod.rs b/polkadot/runtime/common/src/slots/mod.rs index 333f14c6608a..a733341bd3c7 100644 --- a/polkadot/runtime/common/src/slots/mod.rs +++ b/polkadot/runtime/common/src/slots/mod.rs @@ -497,653 +497,11 @@ impl Leaser> for Pallet { } } -/// tests for this pallet #[cfg(test)] -mod tests { - use super::*; - - use crate::{mock::TestRegistrar, slots}; - use frame_support::{assert_noop, assert_ok, derive_impl, parameter_types}; - use frame_system::EnsureRoot; - use pallet_balances; - use polkadot_primitives::BlockNumber; - use polkadot_primitives_test_helpers::{dummy_head_data, dummy_validation_code}; - use sp_core::H256; - use sp_runtime::{ - traits::{BlakeTwo256, IdentityLookup}, - BuildStorage, - }; - - type Block = frame_system::mocking::MockBlockU32; - - frame_support::construct_runtime!( - pub enum Test - { - System: frame_system, - Balances: pallet_balances, - Slots: slots, - } - ); - - #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] - impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = u64; - type Lookup = IdentityLookup; - type Block = Block; - type RuntimeEvent = RuntimeEvent; - type DbWeight = (); - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; - } - - #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] - impl pallet_balances::Config for Test { - type AccountStore = System; - } - - parameter_types! { - pub const LeasePeriod: BlockNumber = 10; - pub static LeaseOffset: BlockNumber = 0; - pub const ParaDeposit: u64 = 1; - } - - impl Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type Registrar = TestRegistrar; - type LeasePeriod = LeasePeriod; - type LeaseOffset = LeaseOffset; - type ForceOrigin = EnsureRoot; - type WeightInfo = crate::slots::TestWeightInfo; - } - - // This function basically just builds a genesis storage key/value store according to - // our desired mock up. - pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(1, 10), (2, 20), (3, 30), (4, 40), (5, 50), (6, 60)], - } - .assimilate_storage(&mut t) - .unwrap(); - t.into() - } - - fn run_to_block(n: BlockNumber) { - while System::block_number() < n { - Slots::on_finalize(System::block_number()); - Balances::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()); - Balances::on_initialize(System::block_number()); - Slots::on_initialize(System::block_number()); - } - } - - #[test] - fn basic_setup_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_eq!(Slots::lease_period_length(), (10, 0)); - let now = System::block_number(); - assert_eq!(Slots::lease_period_index(now).unwrap().0, 0); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - - run_to_block(10); - let now = System::block_number(); - assert_eq!(Slots::lease_period_index(now).unwrap().0, 1); - }); - } - - #[test] - fn lease_lifecycle_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - - assert_ok!(Slots::lease_out(1.into(), &1, 1, 1, 1)); - assert_eq!(Slots::deposit_held(1.into(), &1), 1); - assert_eq!(Balances::reserved_balance(1), 1); - - run_to_block(19); - assert_eq!(Slots::deposit_held(1.into(), &1), 1); - assert_eq!(Balances::reserved_balance(1), 1); - - run_to_block(20); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - - assert_eq!( - TestRegistrar::::operations(), - vec![(1.into(), 10, true), (1.into(), 20, false),] - ); - }); - } - - #[test] - fn lease_interrupted_lifecycle_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - - assert_ok!(Slots::lease_out(1.into(), &1, 6, 1, 1)); - assert_ok!(Slots::lease_out(1.into(), &1, 4, 3, 1)); - - run_to_block(19); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - - run_to_block(20); - assert_eq!(Slots::deposit_held(1.into(), &1), 4); - assert_eq!(Balances::reserved_balance(1), 4); - - run_to_block(39); - assert_eq!(Slots::deposit_held(1.into(), &1), 4); - assert_eq!(Balances::reserved_balance(1), 4); - - run_to_block(40); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - - assert_eq!( - TestRegistrar::::operations(), - vec![ - (1.into(), 10, true), - (1.into(), 20, false), - (1.into(), 30, true), - (1.into(), 40, false), - ] - ); - }); - } - - #[test] - fn lease_relayed_lifecycle_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - - assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); - assert!(Slots::lease_out(1.into(), &2, 4, 2, 1).is_ok()); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - assert_eq!(Slots::deposit_held(1.into(), &2), 4); - assert_eq!(Balances::reserved_balance(2), 4); - - run_to_block(19); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - assert_eq!(Slots::deposit_held(1.into(), &2), 4); - assert_eq!(Balances::reserved_balance(2), 4); - - run_to_block(20); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Slots::deposit_held(1.into(), &2), 4); - assert_eq!(Balances::reserved_balance(2), 4); - - run_to_block(29); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Slots::deposit_held(1.into(), &2), 4); - assert_eq!(Balances::reserved_balance(2), 4); - - run_to_block(30); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - assert_eq!(Slots::deposit_held(1.into(), &2), 0); - assert_eq!(Balances::reserved_balance(2), 0); - - assert_eq!( - TestRegistrar::::operations(), - vec![(1.into(), 10, true), (1.into(), 30, false),] - ); - }); - } - - #[test] - fn lease_deposit_increase_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - - assert!(Slots::lease_out(1.into(), &1, 4, 1, 1).is_ok()); - assert_eq!(Slots::deposit_held(1.into(), &1), 4); - assert_eq!(Balances::reserved_balance(1), 4); - - assert!(Slots::lease_out(1.into(), &1, 6, 2, 1).is_ok()); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - - run_to_block(29); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - - run_to_block(30); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - - assert_eq!( - TestRegistrar::::operations(), - vec![(1.into(), 10, true), (1.into(), 30, false),] - ); - }); - } - - #[test] - fn lease_deposit_decrease_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - - assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - - assert!(Slots::lease_out(1.into(), &1, 4, 2, 1).is_ok()); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - - run_to_block(19); - assert_eq!(Slots::deposit_held(1.into(), &1), 6); - assert_eq!(Balances::reserved_balance(1), 6); - - run_to_block(20); - assert_eq!(Slots::deposit_held(1.into(), &1), 4); - assert_eq!(Balances::reserved_balance(1), 4); - - run_to_block(29); - assert_eq!(Slots::deposit_held(1.into(), &1), 4); - assert_eq!(Balances::reserved_balance(1), 4); - - run_to_block(30); - assert_eq!(Slots::deposit_held(1.into(), &1), 0); - assert_eq!(Balances::reserved_balance(1), 0); - - assert_eq!( - TestRegistrar::::operations(), - vec![(1.into(), 10, true), (1.into(), 30, false),] - ); - }); - } - - #[test] - fn clear_all_leases_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - - let max_num = 5u32; - - // max_num different people are reserved for leases to Para ID 1 - for i in 1u32..=max_num { - let j: u64 = i.into(); - assert_ok!(Slots::lease_out(1.into(), &j, j * 10 - 1, i * i, i)); - assert_eq!(Slots::deposit_held(1.into(), &j), j * 10 - 1); - assert_eq!(Balances::reserved_balance(j), j * 10 - 1); - } - - assert_ok!(Slots::clear_all_leases(RuntimeOrigin::root(), 1.into())); - - // Balances cleaned up correctly - for i in 1u32..=max_num { - let j: u64 = i.into(); - assert_eq!(Slots::deposit_held(1.into(), &j), 0); - assert_eq!(Balances::reserved_balance(j), 0); - } - - // Leases is empty. - assert!(Leases::::get(ParaId::from(1_u32)).is_empty()); - }); - } - - #[test] - fn lease_out_current_lease_period() { - new_test_ext().execute_with(|| { - run_to_block(1); - - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(2_u32), - dummy_head_data(), - dummy_validation_code() - )); - - run_to_block(20); - let now = System::block_number(); - assert_eq!(Slots::lease_period_index(now).unwrap().0, 2); - // Can't lease from the past - assert!(Slots::lease_out(1.into(), &1, 1, 1, 1).is_err()); - // Lease in the current period triggers onboarding - assert_ok!(Slots::lease_out(1.into(), &1, 1, 2, 1)); - // Lease in the future doesn't - assert_ok!(Slots::lease_out(2.into(), &1, 1, 3, 1)); - - assert_eq!(TestRegistrar::::operations(), vec![(1.into(), 20, true),]); - }); - } - - #[test] - fn trigger_onboard_works() { - new_test_ext().execute_with(|| { - run_to_block(1); - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(1_u32), - dummy_head_data(), - dummy_validation_code() - )); - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(2_u32), - dummy_head_data(), - dummy_validation_code() - )); - assert_ok!(TestRegistrar::::register( - 1, - ParaId::from(3_u32), - dummy_head_data(), - dummy_validation_code() - )); - - // We will directly manipulate leases to emulate some kind of failure in the system. - // Para 1 will have no leases - // Para 2 will have a lease period in the current index - Leases::::insert(ParaId::from(2_u32), vec![Some((0, 0))]); - // Para 3 will have a lease period in a future index - Leases::::insert(ParaId::from(3_u32), vec![None, None, Some((0, 0))]); - - // Para 1 should fail cause they don't have any leases - assert_noop!( - Slots::trigger_onboard(RuntimeOrigin::signed(1), 1.into()), - Error::::ParaNotOnboarding - ); - - // Para 2 should succeed - assert_ok!(Slots::trigger_onboard(RuntimeOrigin::signed(1), 2.into())); - - // Para 3 should fail cause their lease is in the future - assert_noop!( - Slots::trigger_onboard(RuntimeOrigin::signed(1), 3.into()), - Error::::ParaNotOnboarding - ); - - // Trying Para 2 again should fail cause they are not currently an on-demand parachain - assert!(Slots::trigger_onboard(RuntimeOrigin::signed(1), 2.into()).is_err()); - - assert_eq!(TestRegistrar::::operations(), vec![(2.into(), 1, true),]); - }); - } +mod mock; - #[test] - fn lease_period_offset_works() { - new_test_ext().execute_with(|| { - let (lpl, offset) = Slots::lease_period_length(); - assert_eq!(offset, 0); - assert_eq!(Slots::lease_period_index(0), Some((0, true))); - assert_eq!(Slots::lease_period_index(1), Some((0, false))); - assert_eq!(Slots::lease_period_index(lpl - 1), Some((0, false))); - assert_eq!(Slots::lease_period_index(lpl), Some((1, true))); - assert_eq!(Slots::lease_period_index(lpl + 1), Some((1, false))); - assert_eq!(Slots::lease_period_index(2 * lpl - 1), Some((1, false))); - assert_eq!(Slots::lease_period_index(2 * lpl), Some((2, true))); - assert_eq!(Slots::lease_period_index(2 * lpl + 1), Some((2, false))); - - // Lease period is 10, and we add an offset of 5. - LeaseOffset::set(5); - let (lpl, offset) = Slots::lease_period_length(); - assert_eq!(offset, 5); - assert_eq!(Slots::lease_period_index(0), None); - assert_eq!(Slots::lease_period_index(1), None); - assert_eq!(Slots::lease_period_index(offset), Some((0, true))); - assert_eq!(Slots::lease_period_index(lpl), Some((0, false))); - assert_eq!(Slots::lease_period_index(lpl - 1 + offset), Some((0, false))); - assert_eq!(Slots::lease_period_index(lpl + offset), Some((1, true))); - assert_eq!(Slots::lease_period_index(lpl + offset + 1), Some((1, false))); - assert_eq!(Slots::lease_period_index(2 * lpl - 1 + offset), Some((1, false))); - assert_eq!(Slots::lease_period_index(2 * lpl + offset), Some((2, true))); - assert_eq!(Slots::lease_period_index(2 * lpl + offset + 1), Some((2, false))); - }); - } -} +#[cfg(test)] +mod tests; #[cfg(feature = "runtime-benchmarks")] -mod benchmarking { - use super::*; - use frame_support::assert_ok; - use frame_system::RawOrigin; - use polkadot_runtime_parachains::paras; - use sp_runtime::traits::{Bounded, One}; - - use frame_benchmarking::{account, benchmarks, whitelisted_caller, BenchmarkError}; - - use crate::slots::Pallet as Slots; - - fn assert_last_event(generic_event: ::RuntimeEvent) { - let events = frame_system::Pallet::::events(); - let system_event: ::RuntimeEvent = generic_event.into(); - // compare to the last event record - let frame_system::EventRecord { event, .. } = &events[events.len() - 1]; - assert_eq!(event, &system_event); - } - - // Registers a parathread (on-demand parachain) - fn register_a_parathread(i: u32) -> (ParaId, T::AccountId) { - let para = ParaId::from(i); - let leaser: T::AccountId = account("leaser", i, 0); - T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); - let worst_head_data = T::Registrar::worst_head_data(); - let worst_validation_code = T::Registrar::worst_validation_code(); - - assert_ok!(T::Registrar::register( - leaser.clone(), - para, - worst_head_data, - worst_validation_code.clone(), - )); - assert_ok!(paras::Pallet::::add_trusted_validation_code( - frame_system::Origin::::Root.into(), - worst_validation_code, - )); - - T::Registrar::execute_pending_transitions(); - - (para, leaser) - } - - benchmarks! { - where_clause { where T: paras::Config } - - force_lease { - // If there is an offset, we need to be on that block to be able to do lease things. - frame_system::Pallet::::set_block_number(T::LeaseOffset::get() + One::one()); - let para = ParaId::from(1337); - let leaser: T::AccountId = account("leaser", 0, 0); - T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); - let amount = T::Currency::minimum_balance(); - let period_begin = 69u32.into(); - let period_count = 3u32.into(); - let origin = - T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: _(origin, para, leaser.clone(), amount, period_begin, period_count) - verify { - assert_last_event::(Event::::Leased { - para_id: para, - leaser, period_begin, - period_count, - extra_reserved: amount, - total_amount: amount, - }.into()); - } - - // Worst case scenario, T on-demand parachains onboard, and C lease holding parachains offboard. - manage_lease_period_start { - // Assume reasonable maximum of 100 paras at any time - let c in 0 .. 100; - let t in 0 .. 100; - - let period_begin = 1u32.into(); - let period_count = 4u32.into(); - - // If there is an offset, we need to be on that block to be able to do lease things. - frame_system::Pallet::::set_block_number(T::LeaseOffset::get() + One::one()); - - // Make T parathreads (on-demand parachains) - let paras_info = (0..t).map(|i| { - register_a_parathread::(i) - }).collect::>(); - - T::Registrar::execute_pending_transitions(); - - // T on-demand parachains are upgrading to lease holding parachains - for (para, leaser) in paras_info { - let amount = T::Currency::minimum_balance(); - let origin = T::ForceOrigin::try_successful_origin() - .expect("ForceOrigin has no successful origin required for the benchmark"); - Slots::::force_lease(origin, para, leaser, amount, period_begin, period_count)?; - } - - T::Registrar::execute_pending_transitions(); - - // C lease holding parachains are downgrading to on-demand parachains - for i in 200 .. 200 + c { - let (para, leaser) = register_a_parathread::(i); - T::Registrar::make_parachain(para)?; - } - - T::Registrar::execute_pending_transitions(); - - for i in 0 .. t { - assert!(T::Registrar::is_parathread(ParaId::from(i))); - } - - for i in 200 .. 200 + c { - assert!(T::Registrar::is_parachain(ParaId::from(i))); - } - }: { - Slots::::manage_lease_period_start(period_begin); - } verify { - // All paras should have switched. - T::Registrar::execute_pending_transitions(); - for i in 0 .. t { - assert!(T::Registrar::is_parachain(ParaId::from(i))); - } - for i in 200 .. 200 + c { - assert!(T::Registrar::is_parathread(ParaId::from(i))); - } - } - - // Assume that at most 8 people have deposits for leases on a parachain. - // This would cover at least 4 years of leases in the worst case scenario. - clear_all_leases { - let max_people = 8; - let (para, _) = register_a_parathread::(1); - - // If there is an offset, we need to be on that block to be able to do lease things. - frame_system::Pallet::::set_block_number(T::LeaseOffset::get() + One::one()); - - for i in 0 .. max_people { - let leaser = account("lease_deposit", i, 0); - let amount = T::Currency::minimum_balance(); - T::Currency::make_free_balance_be(&leaser, BalanceOf::::max_value()); - - // Average slot has 4 lease periods. - let period_count: LeasePeriodOf = 4u32.into(); - let period_begin = period_count * i.into(); - let origin = T::ForceOrigin::try_successful_origin() - .expect("ForceOrigin has no successful origin required for the benchmark"); - Slots::::force_lease(origin, para, leaser, amount, period_begin, period_count)?; - } - - for i in 0 .. max_people { - let leaser = account("lease_deposit", i, 0); - assert_eq!(T::Currency::reserved_balance(&leaser), T::Currency::minimum_balance()); - } - - let origin = - T::ForceOrigin::try_successful_origin().map_err(|_| BenchmarkError::Weightless)?; - }: _(origin, para) - verify { - for i in 0 .. max_people { - let leaser = account("lease_deposit", i, 0); - assert_eq!(T::Currency::reserved_balance(&leaser), 0u32.into()); - } - } - - trigger_onboard { - // get a parachain into a bad state where they did not onboard - let (para, _) = register_a_parathread::(1); - Leases::::insert(para, vec![Some((account::("lease_insert", 0, 0), BalanceOf::::default()))]); - assert!(T::Registrar::is_parathread(para)); - let caller = whitelisted_caller(); - }: _(RawOrigin::Signed(caller), para) - verify { - T::Registrar::execute_pending_transitions(); - assert!(T::Registrar::is_parachain(para)); - } - - impl_benchmark_test_suite!( - Slots, - crate::integration_tests::new_test_ext(), - crate::integration_tests::Test, - ); - } -} +mod benchmarking; diff --git a/polkadot/runtime/common/src/slots/tests.rs b/polkadot/runtime/common/src/slots/tests.rs new file mode 100644 index 000000000000..a5477fea1ef9 --- /dev/null +++ b/polkadot/runtime/common/src/slots/tests.rs @@ -0,0 +1,392 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// This file is part of Polkadot. + +// Polkadot is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Polkadot is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Polkadot. If not, see . + +//! Tests for the slots pallet. + +#[cfg(test)] +use super::*; + +use crate::{mock::TestRegistrar, slots::mock::*}; +use frame_support::{assert_noop, assert_ok}; +use polkadot_primitives_test_helpers::{dummy_head_data, dummy_validation_code}; + +#[test] +fn basic_setup_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_eq!(Slots::lease_period_length(), (10, 0)); + let now = System::block_number(); + assert_eq!(Slots::lease_period_index(now).unwrap().0, 0); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + + run_to_block(10); + let now = System::block_number(); + assert_eq!(Slots::lease_period_index(now).unwrap().0, 1); + }); +} + +#[test] +fn lease_lifecycle_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + + assert_ok!(Slots::lease_out(1.into(), &1, 1, 1, 1)); + assert_eq!(Slots::deposit_held(1.into(), &1), 1); + assert_eq!(Balances::reserved_balance(1), 1); + + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 1); + assert_eq!(Balances::reserved_balance(1), 1); + + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + + assert_eq!( + TestRegistrar::::operations(), + vec![(1.into(), 10, true), (1.into(), 20, false),] + ); + }); +} + +#[test] +fn lease_interrupted_lifecycle_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + + assert_ok!(Slots::lease_out(1.into(), &1, 6, 1, 1)); + assert_ok!(Slots::lease_out(1.into(), &1, 4, 3, 1)); + + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); + + run_to_block(39); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); + + run_to_block(40); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + + assert_eq!( + TestRegistrar::::operations(), + vec![ + (1.into(), 10, true), + (1.into(), 20, false), + (1.into(), 30, true), + (1.into(), 40, false), + ] + ); + }); +} + +#[test] +fn lease_relayed_lifecycle_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + + assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); + assert!(Slots::lease_out(1.into(), &2, 4, 2, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); + + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); + + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); + + run_to_block(29); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Slots::deposit_held(1.into(), &2), 4); + assert_eq!(Balances::reserved_balance(2), 4); + + run_to_block(30); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + assert_eq!(Slots::deposit_held(1.into(), &2), 0); + assert_eq!(Balances::reserved_balance(2), 0); + + assert_eq!( + TestRegistrar::::operations(), + vec![(1.into(), 10, true), (1.into(), 30, false),] + ); + }); +} + +#[test] +fn lease_deposit_increase_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + + assert!(Slots::lease_out(1.into(), &1, 4, 1, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); + + assert!(Slots::lease_out(1.into(), &1, 6, 2, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + + run_to_block(29); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + + run_to_block(30); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + + assert_eq!( + TestRegistrar::::operations(), + vec![(1.into(), 10, true), (1.into(), 30, false),] + ); + }); +} + +#[test] +fn lease_deposit_decrease_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + + assert!(Slots::lease_out(1.into(), &1, 6, 1, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + + assert!(Slots::lease_out(1.into(), &1, 4, 2, 1).is_ok()); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + + run_to_block(19); + assert_eq!(Slots::deposit_held(1.into(), &1), 6); + assert_eq!(Balances::reserved_balance(1), 6); + + run_to_block(20); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); + + run_to_block(29); + assert_eq!(Slots::deposit_held(1.into(), &1), 4); + assert_eq!(Balances::reserved_balance(1), 4); + + run_to_block(30); + assert_eq!(Slots::deposit_held(1.into(), &1), 0); + assert_eq!(Balances::reserved_balance(1), 0); + + assert_eq!( + TestRegistrar::::operations(), + vec![(1.into(), 10, true), (1.into(), 30, false),] + ); + }); +} + +#[test] +fn clear_all_leases_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + + let max_num = 5u32; + + // max_num different people are reserved for leases to Para ID 1 + for i in 1u32..=max_num { + let j: u64 = i.into(); + assert_ok!(Slots::lease_out(1.into(), &j, j * 10 - 1, i * i, i)); + assert_eq!(Slots::deposit_held(1.into(), &j), j * 10 - 1); + assert_eq!(Balances::reserved_balance(j), j * 10 - 1); + } + + assert_ok!(Slots::clear_all_leases(RuntimeOrigin::root(), 1.into())); + + // Balances cleaned up correctly + for i in 1u32..=max_num { + let j: u64 = i.into(); + assert_eq!(Slots::deposit_held(1.into(), &j), 0); + assert_eq!(Balances::reserved_balance(j), 0); + } + + // Leases is empty. + assert!(Leases::::get(ParaId::from(1_u32)).is_empty()); + }); +} + +#[test] +fn lease_out_current_lease_period() { + new_test_ext().execute_with(|| { + run_to_block(1); + + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(2_u32), + dummy_head_data(), + dummy_validation_code() + )); + + run_to_block(20); + let now = System::block_number(); + assert_eq!(Slots::lease_period_index(now).unwrap().0, 2); + // Can't lease from the past + assert!(Slots::lease_out(1.into(), &1, 1, 1, 1).is_err()); + // Lease in the current period triggers onboarding + assert_ok!(Slots::lease_out(1.into(), &1, 1, 2, 1)); + // Lease in the future doesn't + assert_ok!(Slots::lease_out(2.into(), &1, 1, 3, 1)); + + assert_eq!(TestRegistrar::::operations(), vec![(1.into(), 20, true),]); + }); +} + +#[test] +fn trigger_onboard_works() { + new_test_ext().execute_with(|| { + run_to_block(1); + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(1_u32), + dummy_head_data(), + dummy_validation_code() + )); + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(2_u32), + dummy_head_data(), + dummy_validation_code() + )); + assert_ok!(TestRegistrar::::register( + 1, + ParaId::from(3_u32), + dummy_head_data(), + dummy_validation_code() + )); + + // We will directly manipulate leases to emulate some kind of failure in the system. + // Para 1 will have no leases + // Para 2 will have a lease period in the current index + Leases::::insert(ParaId::from(2_u32), vec![Some((0, 0))]); + // Para 3 will have a lease period in a future index + Leases::::insert(ParaId::from(3_u32), vec![None, None, Some((0, 0))]); + + // Para 1 should fail cause they don't have any leases + assert_noop!( + Slots::trigger_onboard(RuntimeOrigin::signed(1), 1.into()), + Error::::ParaNotOnboarding + ); + + // Para 2 should succeed + assert_ok!(Slots::trigger_onboard(RuntimeOrigin::signed(1), 2.into())); + + // Para 3 should fail cause their lease is in the future + assert_noop!( + Slots::trigger_onboard(RuntimeOrigin::signed(1), 3.into()), + Error::::ParaNotOnboarding + ); + + // Trying Para 2 again should fail cause they are not currently an on-demand parachain + assert!(Slots::trigger_onboard(RuntimeOrigin::signed(1), 2.into()).is_err()); + + assert_eq!(TestRegistrar::::operations(), vec![(2.into(), 1, true),]); + }); +} + +#[test] +fn lease_period_offset_works() { + new_test_ext().execute_with(|| { + let (lpl, offset) = Slots::lease_period_length(); + assert_eq!(offset, 0); + assert_eq!(Slots::lease_period_index(0), Some((0, true))); + assert_eq!(Slots::lease_period_index(1), Some((0, false))); + assert_eq!(Slots::lease_period_index(lpl - 1), Some((0, false))); + assert_eq!(Slots::lease_period_index(lpl), Some((1, true))); + assert_eq!(Slots::lease_period_index(lpl + 1), Some((1, false))); + assert_eq!(Slots::lease_period_index(2 * lpl - 1), Some((1, false))); + assert_eq!(Slots::lease_period_index(2 * lpl), Some((2, true))); + assert_eq!(Slots::lease_period_index(2 * lpl + 1), Some((2, false))); + + // Lease period is 10, and we add an offset of 5. + LeaseOffset::set(5); + let (lpl, offset) = Slots::lease_period_length(); + assert_eq!(offset, 5); + assert_eq!(Slots::lease_period_index(0), None); + assert_eq!(Slots::lease_period_index(1), None); + assert_eq!(Slots::lease_period_index(offset), Some((0, true))); + assert_eq!(Slots::lease_period_index(lpl), Some((0, false))); + assert_eq!(Slots::lease_period_index(lpl - 1 + offset), Some((0, false))); + assert_eq!(Slots::lease_period_index(lpl + offset), Some((1, true))); + assert_eq!(Slots::lease_period_index(lpl + offset + 1), Some((1, false))); + assert_eq!(Slots::lease_period_index(2 * lpl - 1 + offset), Some((1, false))); + assert_eq!(Slots::lease_period_index(2 * lpl + offset), Some((2, true))); + assert_eq!(Slots::lease_period_index(2 * lpl + offset + 1), Some((2, false))); + }); +}