From 9aebac3c69f9088bbaf1b310c81ff82db89ac313 Mon Sep 17 00:00:00 2001 From: Shawn Tabrizi Date: Mon, 11 Jan 2021 16:19:45 -0400 Subject: [PATCH] Crowdloan Updates (#2166) * Rename crowdfund -> crowdloan * allow contribution on behalf of another user * starting some benchmarks * optimization: use append api * Use on_initialize instead of on_finalize * More benchmarks * try to implement partial child storage removal * partial dissolve test * onboard benchmark * begin retirement * on_initialize * remove _ { } * Revert "allow contribution on behalf of another user" This reverts commit b7dd7d1ec751495cee3ddb57133a13c390b020e5. * finish undo of "allow contribution on behalf of another user" * Allow any user to trigger withdraw on closed crowdloan * use transfer instead of withdraw/create pattern * unused warning * Update runtime/common/src/crowdloan.rs Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> * dont need to assign to empty variable Co-authored-by: Kian Paimani <5588131+kianenigma@users.noreply.github.com> --- .../common/src/{crowdfund.rs => crowdloan.rs} | 799 +++++++++++++----- polkadot/runtime/common/src/lib.rs | 2 +- polkadot/runtime/common/src/slots.rs | 4 +- 3 files changed, 576 insertions(+), 229 deletions(-) rename polkadot/runtime/common/src/{crowdfund.rs => crowdloan.rs} (60%) diff --git a/polkadot/runtime/common/src/crowdfund.rs b/polkadot/runtime/common/src/crowdloan.rs similarity index 60% rename from polkadot/runtime/common/src/crowdfund.rs rename to polkadot/runtime/common/src/crowdloan.rs index 715f6a9cfb..ad3629e8cf 100644 --- a/polkadot/runtime/common/src/crowdfund.rs +++ b/polkadot/runtime/common/src/crowdloan.rs @@ -14,7 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Polkadot. If not, see . -//! # Parachain Crowdfunding module +//! # Parachain Crowdloaning module //! //! The point of this module is to allow parachain projects to offer the ability to help fund a //! deposit for the parachain. When the parachain is retired, the funds may be returned. @@ -67,14 +67,15 @@ //! funds ultimately end up in module's fund sub-account. use frame_support::{ - decl_module, decl_storage, decl_event, decl_error, storage::child, ensure, + decl_module, decl_storage, decl_event, decl_error, ensure, + storage::child, traits::{ - Currency, Get, OnUnbalanced, WithdrawReasons, ExistenceRequirement::AllowDeath + Currency, Get, OnUnbalanced, ExistenceRequirement::AllowDeath }, }; use frame_system::ensure_signed; -use sp_runtime::{ModuleId, - traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd} +use sp_runtime::{ModuleId, DispatchResult, + traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd, Bounded} }; use crate::slots; use parity_scale_codec::{Encode, Decode}; @@ -90,22 +91,25 @@ pub type NegativeImbalanceOf = pub trait Config: slots::Config { type Event: From> + Into<::Event>; - /// ModuleID for the crowdfund module. An appropriate value could be ```ModuleId(*b"py/cfund")``` + /// ModuleID for the crowdloan module. An appropriate value could be ```ModuleId(*b"py/cfund")``` type ModuleId: Get; - /// The amount to be held on deposit by the owner of a crowdfund. + /// The amount to be held on deposit by the owner of a crowdloan. type SubmissionDeposit: Get>; - /// The minimum amount that may be contributed into a crowdfund. Should almost certainly be at + /// The minimum amount that may be contributed into a crowdloan. Should almost certainly be at /// least ExistentialDeposit. type MinContribution: Get>; - /// The period of time (in blocks) after an unsuccessful crowdfund ending when + /// The period of time (in blocks) after an unsuccessful crowdloan ending when /// contributors are able to withdraw their funds. After this period, their funds are lost. type RetirementPeriod: Get; /// What to do with funds that were not withdrawn. type OrphanedFunds: OnUnbalanced>; + + /// Max number of storage keys to remove per extrinsic call. + type RemoveKeysLimit: Get; } /// Simple index for identifying a fund. @@ -164,7 +168,7 @@ pub struct FundInfo { } decl_storage! { - trait Store for Module as Crowdfund { + trait Store for Module as Crowdloan { /// Info on all of the funds. Funds get(fn funds): map hasher(twox_64_concat) FundIndex @@ -187,7 +191,7 @@ decl_event! { ::AccountId, Balance = BalanceOf, { - /// Create a new crowdfunding campaign. [fund_index] + /// Create a new crowdloaning campaign. [fund_index] Created(FundIndex), /// Contributed to a crowd sale. [who, fund_index, amount] Contributed(AccountId, FundIndex, Balance), @@ -195,12 +199,17 @@ decl_event! { Withdrew(AccountId, FundIndex, Balance), /// Fund is placed into retirement. [fund_index] Retiring(FundIndex), + /// Fund is partially dissolved, i.e. there are some left over child + /// keys that still need to be killed. [fund_index] + PartiallyDissolved(FundIndex), /// Fund is dissolved. [fund_index] Dissolved(FundIndex), /// The deploy data of the funded parachain is setted. [fund_index] DeployDataFixed(FundIndex), /// Onboarding process for a winning parachain fund is completed. [find_index, parachain_id] Onboarded(FundIndex, ParaId), + /// The result of trying to submit a new bid to the Slots pallet. + HandleBidResult(FundIndex, DispatchResult), } } @@ -231,7 +240,7 @@ decl_error! { UnsetDeployData, /// This fund has already been onboarded. AlreadyOnboard, - /// This crowdfund does not correspond to a parachain. + /// This crowdloan does not correspond to a parachain. NotParachain, /// This parachain still has its deposit. Implies that it has already been offboarded. ParaHasDeposit, @@ -239,11 +248,11 @@ decl_error! { FundsNotReturned, /// Fund has not yet retired. FundNotRetired, - /// The crowdfund has not yet ended. + /// The crowdloan has not yet ended. FundNotEnded, - /// There are no contributions stored in this crowdfund. + /// There are no contributions stored in this crowdloan. NoContributions, - /// This crowdfund has an active parachain and cannot be dissolved. + /// This crowdloan has an active parachain and cannot be dissolved. HasActiveParachain, /// The retirement period has not ended. InRetirementPeriod, @@ -258,7 +267,7 @@ decl_module! { fn deposit_event() = default; - /// Create a new crowdfunding campaign for a parachain slot deposit for the current auction. + /// Create a new crowdloaning campaign for a parachain slot deposit for the current auction. #[weight = 100_000_000] fn create(origin, #[compact] cap: BalanceOf, @@ -272,17 +281,12 @@ decl_module! { ensure!(last_slot <= first_slot + 3u32.into(), Error::::LastSlotTooFarInFuture); ensure!(end > >::block_number(), Error::::CannotEndInPast); - let deposit = T::SubmissionDeposit::get(); - let transfer = WithdrawReasons::TRANSFER; - let imb = T::Currency::withdraw(&owner, deposit, transfer, AllowDeath)?; - let index = FundCount::get(); let next_index = index.checked_add(1).ok_or(Error::::Overflow)?; - FundCount::put(next_index); - // No fees are paid here if we need to create this account; that's why we don't just - // use the stock `transfer`. - T::Currency::resolve_creating(&Self::fund_account_id(index), imb); + let deposit = T::SubmissionDeposit::get(); + T::Currency::transfer(&owner, &Self::fund_account_id(index), deposit, AllowDeath)?; + FundCount::put(next_index); >::insert(index, FundInfo { parachain: None, @@ -312,7 +316,7 @@ decl_module! { fund.raised = fund.raised.checked_add(&value).ok_or(Error::::Overflow)?; ensure!(fund.raised <= fund.cap, Error::::CapExceeded); - // Make sure crowdfund has not ended + // Make sure crowdloan has not ended let now = >::block_number(); ensure!(fund.end > now, Error::::ContributionPeriodOver); @@ -329,7 +333,7 @@ decl_module! { // do nothing - already in NewRaise } _ => { - NewRaise::mutate(|v| v.push(index)); + NewRaise::append(index); fund.last_contribution = LastContribution::Ending(now); } } @@ -344,7 +348,7 @@ decl_module! { _ => { // Not in ending period; but an auction has been ending since our previous // bid, or we never had one to begin with. Add bid. - NewRaise::mutate(|v| v.push(index)); + NewRaise::append(index); fund.last_contribution = LastContribution::PreEnding(endings_count); } } @@ -393,7 +397,7 @@ decl_module! { #[compact] index: FundIndex, #[compact] para_id: ParaId ) { - let _ = ensure_signed(origin)?; + ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; let DeployData { code_hash, code_size, initial_head_data } @@ -419,7 +423,7 @@ decl_module! { /// Note that a successful fund has lost its parachain slot, and place it into retirement. #[weight = 0] fn begin_retirement(origin, #[compact] index: FundIndex) { - let _ = ensure_signed(origin)?; + ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; let parachain_id = fund.parachain.take().ok_or(Error::::NotParachain)?; @@ -440,8 +444,8 @@ decl_module! { /// Withdraw full balance of a contributor to an unsuccessful or off-boarded fund. #[weight = 0] - fn withdraw(origin, #[compact] index: FundIndex) { - let who = ensure_signed(origin)?; + fn withdraw(origin, who: T::AccountId, #[compact] index: FundIndex) { + ensure_signed(origin)?; let mut fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; ensure!(fund.parachain.is_none(), Error::::FundNotRetired); @@ -454,10 +458,8 @@ decl_module! { ensure!(balance > Zero::zero(), Error::::NoContributions); // Avoid using transfer to ensure we don't pay any fees. - let fund_account = &Self::fund_account_id(index); - let transfer = WithdrawReasons::TRANSFER; - let imbalance = T::Currency::withdraw(fund_account, balance, transfer, AllowDeath)?; - let _ = T::Currency::resolve_into_existing(&who, imbalance); + let fund_account = Self::fund_account_id(index); + T::Currency::transfer(&fund_account, &who, balance, AllowDeath)?; Self::contribution_kill(index, &who); fund.raised = fund.raised.saturating_sub(balance); @@ -472,7 +474,7 @@ decl_module! { /// withdrawn into the treasury. #[weight = 0] fn dissolve(origin, #[compact] index: FundIndex) { - let _ = ensure_signed(origin)?; + ensure_signed(origin)?; let fund = Self::funds(index).ok_or(Error::::InvalidFundIndex)?; ensure!(fund.parachain.is_none(), Error::::HasActiveParachain); @@ -482,23 +484,27 @@ decl_module! { Error::::InRetirementPeriod ); - let account = Self::fund_account_id(index); + // Try killing the crowdloan child trie + match Self::crowdloan_kill(index) { + child::KillOutcome::AllRemoved => { + let account = Self::fund_account_id(index); + T::Currency::transfer(&account, &fund.owner, fund.deposit, AllowDeath)?; - // Avoid using transfer to ensure we don't pay any fees. - let transfer = WithdrawReasons::TRANSFER; - let imbalance = T::Currency::withdraw(&account, fund.deposit, transfer, AllowDeath)?; - let _ = T::Currency::resolve_into_existing(&fund.owner, imbalance); + // Remove all other balance from the account into orphaned funds. + let (imbalance, _) = T::Currency::slash(&account, BalanceOf::::max_value()); + T::OrphanedFunds::on_unbalanced(imbalance); - let imbalance = T::Currency::withdraw(&account, fund.raised, transfer, AllowDeath)?; - T::OrphanedFunds::on_unbalanced(imbalance); + >::remove(index); - Self::crowdfund_kill(index); - >::remove(index); - - Self::deposit_event(RawEvent::Dissolved(index)); + Self::deposit_event(RawEvent::Dissolved(index)); + }, + child::KillOutcome::SomeRemaining => { + Self::deposit_event(RawEvent::PartiallyDissolved(index)); + } + } } - fn on_finalize(n: T::BlockNumber) { + fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight { if let Some(n) = >::is_ending(n) { let auction_index = >::auction_counter(); if n.is_zero() { @@ -513,17 +519,21 @@ decl_module! { sub: index, }); - // Care needs to be taken by the crowdfund creator that this function will succeed given - // the crowdfunding configuration. We do some checks ahead of time in crowdfund `create`. - let _ = >::handle_bid( + // Care needs to be taken by the crowdloan creator that this function will succeed given + // the crowdloaning configuration. We do some checks ahead of time in crowdloan `create`. + let result = >::handle_bid( bidder, auction_index, fund.first_slot, fund.last_slot, fund.raised, ); + + Self::deposit_event(RawEvent::HandleBidResult(index, result)); } } + + 0 } } } @@ -539,7 +549,7 @@ impl Module { pub fn id_from_index(index: FundIndex) -> child::ChildInfo { let mut buf = Vec::new(); - buf.extend_from_slice(b"crowdfund"); + buf.extend_from_slice(b"crowdloan"); buf.extend_from_slice(&index.to_le_bytes()[..]); child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref()) } @@ -559,8 +569,8 @@ impl Module { who.using_encoded(|b| child::kill(&Self::id_from_index(index), b)); } - pub fn crowdfund_kill(index: FundIndex) { - child::kill_storage(&Self::id_from_index(index), None); + pub fn crowdloan_kill(index: FundIndex) -> child::KillOutcome { + child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get())) } } @@ -570,7 +580,7 @@ mod tests { use std::{collections::HashMap, cell::RefCell}; use frame_support::{ - impl_outer_origin, assert_ok, assert_noop, parameter_types, + impl_outer_origin, impl_outer_event, assert_ok, assert_noop, parameter_types, traits::{OnInitialize, OnFinalize}, }; use sp_core::H256; @@ -578,7 +588,7 @@ mod tests { // 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 requried. use sp_runtime::{ - Permill, testing::Header, DispatchResult, + Permill, testing::Header, traits::{BlakeTwo256, IdentityLookup}, }; use crate::slots::Registrar; @@ -587,6 +597,24 @@ mod tests { pub enum Origin for Test {} } + mod runtime_common_slots { + pub use crate::slots::Event; + } + + mod runtime_common_crowdloan { + pub use crate::crowdloan::Event; + } + + impl_outer_event! { + pub enum Event for Test { + frame_system, + pallet_balances, + pallet_treasury, + runtime_common_slots, + runtime_common_crowdloan, + } + } + // For testing the module, we construct most of a mock runtime. This means // first constructing a configuration type (`Test`) which `impl`s each of the // configuration traits of modules we want to use. @@ -610,7 +638,7 @@ mod tests { type AccountId = u64; type Lookup = IdentityLookup; type Header = Header; - type Event = (); + type Event = Event; type BlockHashCount = BlockHashCount; type Version = (); type PalletInfo = (); @@ -625,7 +653,7 @@ mod tests { } impl pallet_balances::Config for Test { type Balance = u64; - type Event = (); + type Event = Event; type DustRemoval = (); type ExistentialDeposit = ExistentialDeposit; type AccountStore = System; @@ -644,7 +672,7 @@ mod tests { type Currency = pallet_balances::Module; type ApproveOrigin = frame_system::EnsureRoot; type RejectOrigin = frame_system::EnsureRoot; - type Event = (); + type Event = Event; type OnSlash = (); type ProposalBond = ProposalBond; type ProposalBondMinimum = ProposalBondMinimum; @@ -713,7 +741,7 @@ mod tests { pub const EndingPeriod: u64 = 3; } impl slots::Config for Test { - type Event = (); + type Event = Event; type Currency = Balances; type Parachains = TestParachains; type LeasePeriod = LeasePeriod; @@ -724,29 +752,31 @@ mod tests { pub const SubmissionDeposit: u64 = 1; pub const MinContribution: u64 = 10; pub const RetirementPeriod: u64 = 5; - pub const CrowdfundModuleId: ModuleId = ModuleId(*b"py/cfund"); + pub const CrowdloanModuleId: ModuleId = ModuleId(*b"py/cfund"); + pub const RemoveKeysLimit: u32 = 10; } impl Config for Test { - type Event = (); + type Event = Event; type SubmissionDeposit = SubmissionDeposit; type MinContribution = MinContribution; type RetirementPeriod = RetirementPeriod; type OrphanedFunds = Treasury; - type ModuleId = CrowdfundModuleId; + type ModuleId = CrowdloanModuleId; + type RemoveKeysLimit = RemoveKeysLimit; } type System = frame_system::Module; type Balances = pallet_balances::Module; type Slots = slots::Module; type Treasury = pallet_treasury::Module; - type Crowdfund = Module; + type Crowdloan = Module; type RandomnessCollectiveFlip = pallet_randomness_collective_flip::Module; use pallet_balances::Error as BalancesError; use slots::Error as SlotsError; // This function basically just builds a genesis storage key/value store according to // our desired mockup. - fn new_test_ext() -> sp_io::TestExternalities { + 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)], @@ -756,7 +786,7 @@ mod tests { fn run_to_block(n: u64) { while System::block_number() < n { - Crowdfund::on_finalize(System::block_number()); + Crowdloan::on_finalize(System::block_number()); Treasury::on_finalize(System::block_number()); Slots::on_finalize(System::block_number()); Balances::on_finalize(System::block_number()); @@ -766,7 +796,7 @@ mod tests { Balances::on_initialize(System::block_number()); Slots::on_initialize(System::block_number()); Treasury::on_initialize(System::block_number()); - Crowdfund::on_initialize(System::block_number()); + Crowdloan::on_initialize(System::block_number()); } } @@ -774,21 +804,21 @@ mod tests { fn basic_setup_works() { new_test_ext().execute_with(|| { assert_eq!(System::block_number(), 0); - assert_eq!(Crowdfund::fund_count(), 0); - assert_eq!(Crowdfund::funds(0), None); + assert_eq!(Crowdloan::fund_count(), 0); + assert_eq!(Crowdloan::funds(0), None); let empty: Vec = Vec::new(); - assert_eq!(Crowdfund::new_raise(), empty); - assert_eq!(Crowdfund::contribution_get(0, &1), 0); - assert_eq!(Crowdfund::endings_count(), 0); + assert_eq!(Crowdloan::new_raise(), empty); + assert_eq!(Crowdloan::contribution_get(0, &1), 0); + assert_eq!(Crowdloan::endings_count(), 0); }); } #[test] fn create_works() { new_test_ext().execute_with(|| { - // Now try to create a crowdfund campaign - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); - assert_eq!(Crowdfund::fund_count(), 1); + // Now try to create a crowdloan campaign + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_eq!(Crowdloan::fund_count(), 1); // This is what the initial `fund_info` should look like let fund_info = FundInfo { parachain: None, @@ -803,33 +833,33 @@ mod tests { last_slot: 4, deploy_data: None, }; - assert_eq!(Crowdfund::funds(0), Some(fund_info)); + assert_eq!(Crowdloan::funds(0), Some(fund_info)); // User has deposit removed from their free balance assert_eq!(Balances::free_balance(1), 999); - // Deposit is placed in crowdfund free balance - assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); + // Deposit is placed in crowdloan free balance + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1); // No new raise until first contribution let empty: Vec = Vec::new(); - assert_eq!(Crowdfund::new_raise(), empty); + assert_eq!(Crowdloan::new_raise(), empty); }); } #[test] fn create_handles_basic_errors() { new_test_ext().execute_with(|| { - // Cannot create a crowdfund with bad slots + // Cannot create a crowdloan with bad slots assert_noop!( - Crowdfund::create(Origin::signed(1), 1000, 4, 1, 9), + Crowdloan::create(Origin::signed(1), 1000, 4, 1, 9), Error::::LastSlotBeforeFirstSlot ); assert_noop!( - Crowdfund::create(Origin::signed(1), 1000, 1, 5, 9), + Crowdloan::create(Origin::signed(1), 1000, 1, 5, 9), Error::::LastSlotTooFarInFuture ); - // Cannot create a crowdfund without some deposit funds + // Cannot create a crowdloan without some deposit funds assert_noop!( - Crowdfund::create(Origin::signed(1337), 1000, 1, 3, 9), + Crowdloan::create(Origin::signed(1337), 1000, 1, 3, 9), BalancesError::::InsufficientBalance ); }); @@ -838,26 +868,26 @@ mod tests { #[test] fn contribute_works() { new_test_ext().execute_with(|| { - // Set up a crowdfund - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + // Set up a crowdloan + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); - assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 1); + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1); // No contributions yet - assert_eq!(Crowdfund::contribution_get(0, &1), 0); + assert_eq!(Crowdloan::contribution_get(0, &1), 0); - // User 1 contributes to their own crowdfund - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); + // User 1 contributes to their own crowdloan + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49)); // 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!(Crowdfund::contribution_get(0, &1), 49); - // Contributions appear in free balance of crowdfund - assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 50); - // Crowdfund is added to NewRaise - assert_eq!(Crowdfund::new_raise(), vec![0]); + assert_eq!(Crowdloan::contribution_get(0, &1), 49); + // Contributions appear in free balance of crowdloan + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 50); + // Crowdloan is added to NewRaise + assert_eq!(Crowdloan::new_raise(), vec![0]); - let fund = Crowdfund::funds(0).unwrap(); + let fund = Crowdloan::funds(0).unwrap(); // Last contribution time recorded assert_eq!(fund.last_contribution, LastContribution::PreEnding(0)); @@ -869,34 +899,34 @@ mod tests { fn contribute_handles_basic_errors() { new_test_ext().execute_with(|| { // Cannot contribute to non-existing fund - assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 49), Error::::InvalidFundIndex); + assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49), Error::::InvalidFundIndex); // Cannot contribute below minimum contribution - assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 9), Error::::ContributionTooSmall); + assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 9), Error::::ContributionTooSmall); - // Set up a crowdfund - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 101)); + // Set up a crowdloan + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 101)); // Cannot contribute past the limit - assert_noop!(Crowdfund::contribute(Origin::signed(2), 0, 900), Error::::CapExceeded); + assert_noop!(Crowdloan::contribute(Origin::signed(2), 0, 900), Error::::CapExceeded); // Move past end date run_to_block(10); // Cannot contribute to ended fund - assert_noop!(Crowdfund::contribute(Origin::signed(1), 0, 49), Error::::ContributionPeriodOver); + assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49), Error::::ContributionPeriodOver); }); } #[test] fn fix_deploy_data_works() { new_test_ext().execute_with(|| { - // Set up a crowdfund - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + // Set up a crowdloan + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -904,7 +934,7 @@ mod tests { vec![0].into() )); - let fund = Crowdfund::funds(0).unwrap(); + let fund = Crowdloan::funds(0).unwrap(); // Confirm deploy data is stored correctly assert_eq!( @@ -921,12 +951,12 @@ mod tests { #[test] fn fix_deploy_data_handles_basic_errors() { new_test_ext().execute_with(|| { - // Set up a crowdfund - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + // Set up a crowdloan + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Cannot set deploy data by non-owner - assert_noop!(Crowdfund::fix_deploy_data( + assert_noop!(Crowdloan::fix_deploy_data( Origin::signed(2), 0, ::Hash::default(), @@ -936,7 +966,7 @@ mod tests { ); // Cannot set deploy data to an invalid index - assert_noop!(Crowdfund::fix_deploy_data( + assert_noop!(Crowdloan::fix_deploy_data( Origin::signed(1), 1, ::Hash::default(), @@ -946,7 +976,7 @@ mod tests { ); // Cannot set deploy data after it already has been set - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -954,7 +984,7 @@ mod tests { vec![0].into(), )); - assert_noop!(Crowdfund::fix_deploy_data( + assert_noop!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -968,13 +998,13 @@ mod tests { #[test] fn onboard_works() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -982,19 +1012,19 @@ mod tests { vec![0].into(), )); - // Fund crowdfund - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); + // Fund crowdloan + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Endings count incremented - assert_eq!(Crowdfund::endings_count(), 1); + assert_eq!(Crowdloan::endings_count(), 1); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - let fund = Crowdfund::funds(0).unwrap(); - // Crowdfund is now assigned a parachain id + let fund = Crowdloan::funds(0).unwrap(); + // Crowdloan is now assigned a parachain id assert_eq!(fund.parachain, Some(0.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into()]); @@ -1004,23 +1034,23 @@ mod tests { #[test] fn onboard_handles_basic_errors() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); - // Fund crowdfund - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); + // Fund crowdloan + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Cannot onboard invalid fund index - assert_noop!(Crowdfund::onboard(Origin::signed(1), 1, 0.into()), Error::::InvalidFundIndex); - // Cannot onboard crowdfund without deploy data - assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 0.into()), Error::::UnsetDeployData); + assert_noop!(Crowdloan::onboard(Origin::signed(1), 1, 0.into()), Error::::InvalidFundIndex); + // Cannot onboard crowdloan without deploy data + assert_noop!(Crowdloan::onboard(Origin::signed(1), 0, 0.into()), Error::::UnsetDeployData); // Add deploy data - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -1029,26 +1059,26 @@ mod tests { )); // Cannot onboard fund with incorrect parachain id - assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 1.into()), SlotsError::::ParaNotOnboarding); + assert_noop!(Crowdloan::onboard(Origin::signed(1), 0, 1.into()), SlotsError::::ParaNotOnboarding); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); // Cannot onboard fund again - assert_noop!(Crowdfund::onboard(Origin::signed(1), 0, 0.into()), Error::::AlreadyOnboard); + assert_noop!(Crowdloan::onboard(Origin::signed(1), 0, 0.into()), Error::::AlreadyOnboard); }); } #[test] fn begin_retirement_works() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -1056,27 +1086,27 @@ mod tests { vec![0].into(), )); - // Fund crowdfund - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); + // Fund crowdloan + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); // Fund is assigned a parachain id - let fund = Crowdfund::funds(0).unwrap(); + let fund = Crowdloan::funds(0).unwrap(); assert_eq!(fund.parachain, Some(0.into())); - // Off-boarding is set to the crowdfund account - assert_eq!(Slots::offboarding(ParaId::from(0)), Crowdfund::fund_account_id(0)); + // Off-boarding is set to the crowdloan account + assert_eq!(Slots::offboarding(ParaId::from(0)), Crowdloan::fund_account_id(0)); run_to_block(50); - // Retire crowdfund to remove parachain id - assert_ok!(Crowdfund::begin_retirement(Origin::signed(1), 0)); + // Retire crowdloan to remove parachain id + assert_ok!(Crowdloan::begin_retirement(Origin::signed(1), 0)); // Fund should no longer have parachain id - let fund = Crowdfund::funds(0).unwrap(); + let fund = Crowdloan::funds(0).unwrap(); assert_eq!(fund.parachain, None); }); @@ -1085,13 +1115,13 @@ mod tests { #[test] fn begin_retirement_handles_basic_errors() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); assert_eq!(Balances::free_balance(1), 999); // Add deploy data - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -1099,56 +1129,56 @@ mod tests { vec![0].into(), )); - // Fund crowdfund - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 1000)); + // Fund crowdloan + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000)); run_to_block(10); // Cannot retire fund that is not onboarded - assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); + assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); // Fund is assigned a parachain id - let fund = Crowdfund::funds(0).unwrap(); + let fund = Crowdloan::funds(0).unwrap(); assert_eq!(fund.parachain, Some(0.into())); // Cannot retire fund whose deposit has not been returned - assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), Error::::ParaHasDeposit); + assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 0), Error::::ParaHasDeposit); run_to_block(50); // Cannot retire invalid fund index - assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 1), Error::::InvalidFundIndex); + assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 1), Error::::InvalidFundIndex); // Cannot retire twice - assert_ok!(Crowdfund::begin_retirement(Origin::signed(1), 0)); - assert_noop!(Crowdfund::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); + assert_ok!(Crowdloan::begin_retirement(Origin::signed(1), 0)); + assert_noop!(Crowdloan::begin_retirement(Origin::signed(1), 0), Error::::NotParachain); }); } #[test] fn withdraw_works() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); - assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 100)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 200)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), 0, 300)); // Skip all the way to the end run_to_block(50); - // User can withdraw their full balance without fees - assert_ok!(Crowdfund::withdraw(Origin::signed(1), 0)); + // Anyone can trigger withdraw of a user's balance without fees + assert_ok!(Crowdloan::withdraw(Origin::signed(1337), 1, 0)); assert_eq!(Balances::free_balance(1), 999); - assert_ok!(Crowdfund::withdraw(Origin::signed(2), 0)); + assert_ok!(Crowdloan::withdraw(Origin::signed(1337), 2, 0)); assert_eq!(Balances::free_balance(2), 2000); - assert_ok!(Crowdfund::withdraw(Origin::signed(3), 0)); + assert_ok!(Crowdloan::withdraw(Origin::signed(1337), 3, 0)); assert_eq!(Balances::free_balance(3), 3000); }); } @@ -1156,37 +1186,37 @@ mod tests { #[test] fn withdraw_handles_basic_errors() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49)); assert_eq!(Balances::free_balance(1), 950); run_to_block(5); // Cannot withdraw before fund ends - assert_noop!(Crowdfund::withdraw(Origin::signed(1), 0), Error::::FundNotEnded); + assert_noop!(Crowdloan::withdraw(Origin::signed(1337), 1, 0), Error::::FundNotEnded); run_to_block(10); // Cannot withdraw if they did not contribute - assert_noop!(Crowdfund::withdraw(Origin::signed(2), 0), Error::::NoContributions); + assert_noop!(Crowdloan::withdraw(Origin::signed(1337), 2, 0), Error::::NoContributions); // Cannot withdraw from a non-existent fund - assert_noop!(Crowdfund::withdraw(Origin::signed(1), 1), Error::::InvalidFundIndex); + assert_noop!(Crowdloan::withdraw(Origin::signed(1337), 2, 1), Error::::InvalidFundIndex); }); } #[test] fn dissolve_works() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); - assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 100)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 200)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), 0, 300)); // Skip all the way to the end run_to_block(50); @@ -1194,75 +1224,140 @@ mod tests { // Check initiator's balance. assert_eq!(Balances::free_balance(1), 899); // Check current funds (contributions + deposit) - assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 601); + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 601); - // Dissolve the crowdfund - assert_ok!(Crowdfund::dissolve(Origin::signed(1), 0)); + // Dissolve the crowdloan + assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); // Fund account is emptied - assert_eq!(Balances::free_balance(Crowdfund::fund_account_id(0)), 0); + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 0); // Deposit is returned assert_eq!(Balances::free_balance(1), 900); // Treasury account is filled assert_eq!(Balances::free_balance(Treasury::account_id()), 600); // Storage trie is removed - assert_eq!(Crowdfund::contribution_get(0,&0), 0); + assert_eq!(Crowdloan::contribution_get(0,&0), 0); // Fund storage is removed - assert_eq!(Crowdfund::funds(0), None); + assert_eq!(Crowdloan::funds(0), None); }); } + #[test] + fn partial_dissolve_works() { + let mut ext = new_test_ext(); + ext.execute_with(|| { + // Set up a crowdloan + assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); + assert_ok!(Crowdloan::create(Origin::signed(1), 100_000, 1, 4, 9)); + + // Add lots of contributors, beyond what we can delete in one go. + for i in 0 .. 30 { + Balances::make_free_balance_be(&i, 300); + assert_ok!(Crowdloan::contribute(Origin::signed(i), 0, 100)); + assert_eq!(Crowdloan::contribution_get(0, &i), 100); + } + + // Skip all the way to the end + run_to_block(50); + + // Check current funds (contributions + deposit) + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 100 * 30 + 1); + }); + + ext.commit_all().unwrap(); + ext.execute_with(|| { + // Partially dissolve the crowdloan + assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); + for i in 0 .. 10 { + assert_eq!(Crowdloan::contribution_get(0, &i), 0); + } + for i in 10 .. 30 { + assert_eq!(Crowdloan::contribution_get(0, &i), 100); + } + }); + + ext.commit_all().unwrap(); + ext.execute_with(|| { + // Partially dissolve the crowdloan, again + assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); + for i in 0 .. 20 { + assert_eq!(Crowdloan::contribution_get(0, &i), 0); + } + for i in 20 .. 30 { + assert_eq!(Crowdloan::contribution_get(0, &i), 100); + } + }); + + ext.commit_all().unwrap(); + ext.execute_with(|| { + // Fully dissolve the crowdloan + assert_ok!(Crowdloan::dissolve(Origin::signed(1), 0)); + for i in 0 .. 30 { + assert_eq!(Crowdloan::contribution_get(0, &i), 0); + } + + // Fund account is emptied + assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 0); + // Deposit is returned + assert_eq!(Balances::free_balance(1), 201); + // Treasury account is filled + assert_eq!(Balances::free_balance(Treasury::account_id()), 100 * 30); + // Fund storage is removed + assert_eq!(Crowdloan::funds(0), None); + }); + } + #[test] fn dissolve_handles_basic_errors() { new_test_ext().execute_with(|| { - // Set up a crowdfund + // Set up a crowdloan assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); // Transfer fee is taken here - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 100)); - assert_ok!(Crowdfund::contribute(Origin::signed(2), 0, 200)); - assert_ok!(Crowdfund::contribute(Origin::signed(3), 0, 300)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 100)); + assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 200)); + assert_ok!(Crowdloan::contribute(Origin::signed(3), 0, 300)); // Cannot dissolve an invalid fund index - assert_noop!(Crowdfund::dissolve(Origin::signed(1), 1), Error::::InvalidFundIndex); + assert_noop!(Crowdloan::dissolve(Origin::signed(1), 1), Error::::InvalidFundIndex); // Cannot dissolve a fund in progress - assert_noop!(Crowdfund::dissolve(Origin::signed(1), 0), Error::::InRetirementPeriod); + assert_noop!(Crowdloan::dissolve(Origin::signed(1), 0), Error::::InRetirementPeriod); run_to_block(10); // Onboard fund - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); // Cannot dissolve an active fund - assert_noop!(Crowdfund::dissolve(Origin::signed(1), 0), Error::::HasActiveParachain); + assert_noop!(Crowdloan::dissolve(Origin::signed(1), 0), Error::::HasActiveParachain); }); } #[test] fn fund_before_auction_works() { new_test_ext().execute_with(|| { - // Create a crowdfund before an auction is created - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 9)); + // Create a crowdloan before an auction is created + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9)); // Users can already contribute - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 49)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49)); // Fund added to NewRaise - assert_eq!(Crowdfund::new_raise(), vec![0]); + assert_eq!(Crowdloan::new_raise(), vec![0]); // Some blocks later... run_to_block(2); // Create an auction assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); // Add deploy data - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), @@ -1273,13 +1368,13 @@ mod tests { run_to_block(12); // Endings count incremented - assert_eq!(Crowdfund::endings_count(), 1); + assert_eq!(Crowdloan::endings_count(), 1); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); - let fund = Crowdfund::funds(0).unwrap(); - // Crowdfund is now assigned a parachain id + let fund = Crowdloan::funds(0).unwrap(); + // Crowdloan is now assigned a parachain id assert_eq!(fund.parachain, Some(0.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into()]); @@ -1291,24 +1386,24 @@ mod tests { new_test_ext().execute_with(|| { // Create an auction assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); - // Create two competing crowdfunds, with end dates across multiple auctions - // Each crowdfund is competing for the same slots, so only one can win - assert_ok!(Crowdfund::create(Origin::signed(1), 1000, 1, 4, 30)); - assert_ok!(Crowdfund::create(Origin::signed(2), 1000, 1, 4, 30)); + // Create two competing crowdloans, with end dates across multiple auctions + // Each crowdloan is competing for the same slots, so only one can win + assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 30)); + assert_ok!(Crowdloan::create(Origin::signed(2), 1000, 1, 4, 30)); // Contribute to all, but more money to 0, less to 1 - assert_ok!(Crowdfund::contribute(Origin::signed(1), 0, 300)); - assert_ok!(Crowdfund::contribute(Origin::signed(1), 1, 200)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 300)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 1, 200)); // Add deploy data to all - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(1), 0, ::Hash::default(), 0, vec![0].into(), )); - assert_ok!(Crowdfund::fix_deploy_data( + assert_ok!(Crowdloan::fix_deploy_data( Origin::signed(2), 1, ::Hash::default(), @@ -1318,11 +1413,11 @@ mod tests { // End the current auction, fund 0 wins! run_to_block(10); - assert_eq!(Crowdfund::endings_count(), 1); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(1), 0, 0.into())); - let fund = Crowdfund::funds(0).unwrap(); - // Crowdfund is now assigned a parachain id + assert_eq!(Crowdloan::endings_count(), 1); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into())); + let fund = Crowdloan::funds(0).unwrap(); + // Crowdloan is now assigned a parachain id assert_eq!(fund.parachain, Some(0.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into()]); @@ -1330,18 +1425,270 @@ mod tests { // Create a second auction assert_ok!(Slots::new_auction(Origin::root(), 5, 1)); // Contribute to existing funds add to NewRaise - assert_ok!(Crowdfund::contribute(Origin::signed(1), 1, 10)); + assert_ok!(Crowdloan::contribute(Origin::signed(1), 1, 10)); // End the current auction, fund 1 wins! run_to_block(20); - assert_eq!(Crowdfund::endings_count(), 2); - // Onboard crowdfund - assert_ok!(Crowdfund::onboard(Origin::signed(2), 1, 1.into())); - let fund = Crowdfund::funds(1).unwrap(); - // Crowdfund is now assigned a parachain id + assert_eq!(Crowdloan::endings_count(), 2); + // Onboard crowdloan + assert_ok!(Crowdloan::onboard(Origin::signed(2), 1, 1.into())); + let fund = Crowdloan::funds(1).unwrap(); + // Crowdloan is now assigned a parachain id assert_eq!(fund.parachain, Some(1.into())); // This parachain is managed by Slots assert_eq!(Slots::managed_ids(), vec![0.into(), 1.into()]); }); } } + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking { + use super::{*, Module as Crowdloan}; + use crate::slots::Module as Slots; + use frame_system::RawOrigin; + use frame_support::{ + assert_ok, + traits::OnInitialize, + }; + use sp_runtime::traits::Bounded; + use sp_std::prelude::*; + + use frame_benchmarking::{benchmarks, whitelisted_caller, account, whitelist_account}; + + // TODO: replace with T::Parachains::MAX_CODE_SIZE + const MAX_CODE_SIZE: u32 = 10; + const MAX_HEAD_DATA_SIZE: u32 = 10; + + fn assert_last_event(generic_event: ::Event) { + let events = frame_system::Module::::events(); + let system_event: ::Event = 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(end: T::BlockNumber) -> FundIndex { + let cap = BalanceOf::::max_value(); + let lease_period_index = end / T::LeasePeriod::get(); + let first_slot = lease_period_index; + let last_slot = lease_period_index + 3u32.into(); + + let caller = account("fund_creator", 0, 0); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + + assert_ok!(Crowdloan::::create(RawOrigin::Signed(caller).into(), cap, first_slot, last_slot, end)); + FundCount::get() - 1 + } + + fn contribute_fund(who: &T::AccountId, index: FundIndex) { + T::Currency::make_free_balance_be(&who, BalanceOf::::max_value()); + let value = T::MinContribution::get(); + assert_ok!(Crowdloan::::contribute(RawOrigin::Signed(who.clone()).into(), index, value)); + } + + fn worst_validation_code() -> Vec { + // TODO: replace with T::Parachains::MAX_CODE_SIZE + let mut validation_code = vec![0u8; MAX_CODE_SIZE as usize]; + // Replace first bytes of code with "WASM_MAGIC" to pass validation test. + let _ = validation_code.splice( + ..crate::WASM_MAGIC.len(), + crate::WASM_MAGIC.iter().cloned(), + ).collect::>(); + validation_code + } + + fn worst_deploy_data() -> DeployData { + let validation_code = worst_validation_code::(); + let code = primitives::v1::ValidationCode(validation_code); + // TODO: replace with T::Parachains::MAX_HEAD_DATA_SIZE + let head_data = HeadData(vec![0u8; MAX_HEAD_DATA_SIZE as usize]); + + DeployData { + code_hash: T::Hashing::hash(&code.0), + // TODO: replace with T::Parachains::MAX_CODE_SIZE + code_size: MAX_CODE_SIZE, + initial_head_data: head_data, + } + } + + fn setup_onboarding( + fund_index: FundIndex, + para_id: ParaId, + end_block: T::BlockNumber, + ) -> DispatchResult { + // Matches fund creator in `create_fund` + let fund_creator = account("fund_creator", 0, 0); + let DeployData { code_hash, code_size, initial_head_data } = worst_deploy_data::(); + Crowdloan::::fix_deploy_data( + RawOrigin::Signed(fund_creator).into(), + fund_index, + code_hash, + code_size, + initial_head_data + )?; + + let lease_period_index = end_block / T::LeasePeriod::get(); + Slots::::new_auction(RawOrigin::Root.into(), end_block, lease_period_index)?; + let contributor: T::AccountId = account("contributor", 0, 0); + contribute_fund::(&contributor, fund_index); + + // TODO: Probably should use on_initialize + //Slots::::on_initialize(end_block + T::EndingPeriod::get()); + let onboarding_data = (lease_period_index, crate::slots::IncomingParachain::Unset( + crate::slots::NewBidder { + who: Crowdloan::::fund_account_id(fund_index), + sub: Default::default(), + } + )); + crate::slots::Onboarding::::insert(para_id, onboarding_data); + Ok(()) + } + + benchmarks! { + create { + let cap = BalanceOf::::max_value(); + let first_slot = 0u32.into(); + let last_slot = 3u32.into(); + let end = T::BlockNumber::max_value(); + + let caller: T::AccountId = whitelisted_caller(); + + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + }: _(RawOrigin::Signed(caller), cap, first_slot, last_slot, end) + verify { + assert_last_event::(RawEvent::Created(FundCount::get() - 1).into()) + } + + // Contribute has two arms: PreEnding and Ending, but both are equal complexity. + contribute { + let fund_index = create_fund::(100u32.into()); + let caller: T::AccountId = whitelisted_caller(); + let contribution = T::MinContribution::get(); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + }: _(RawOrigin::Signed(caller.clone()), fund_index, contribution) + 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::(RawEvent::Contributed(caller, fund_index, contribution).into()); + } + + fix_deploy_data { + let fund_index = create_fund::(100u32.into()); + // Matches fund creator in `create_fund` + let caller = account("fund_creator", 0, 0); + + let DeployData { code_hash, code_size, initial_head_data } = worst_deploy_data::(); + + whitelist_account!(caller); + }: _(RawOrigin::Signed(caller), fund_index, code_hash, code_size, initial_head_data) + verify { + assert_last_event::(RawEvent::DeployDataFixed(fund_index).into()); + } + + onboard { + let end_block: T::BlockNumber = 100u32.into(); + let fund_index = create_fund::(end_block); + let para_id = Default::default(); + + setup_onboarding::(fund_index, para_id, end_block)?; + + let caller = whitelisted_caller(); + }: _(RawOrigin::Signed(caller), fund_index, para_id) + verify { + assert_last_event::(RawEvent::Onboarded(fund_index, para_id).into()); + } + + begin_retirement { + let end_block: T::BlockNumber = 100u32.into(); + let fund_index = create_fund::(end_block); + let para_id = Default::default(); + + setup_onboarding::(fund_index, para_id, end_block)?; + + let caller: T::AccountId = whitelisted_caller(); + Crowdloan::::onboard(RawOrigin::Signed(caller.clone()).into(), fund_index, para_id)?; + + // Remove deposits to look like it is off-boarded + crate::slots::Deposits::::remove(para_id); + }: _(RawOrigin::Signed(caller), fund_index) + verify { + assert_last_event::(RawEvent::Retiring(fund_index).into()); + } + + withdraw { + let fund_index = create_fund::(100u32.into()); + let caller: T::AccountId = whitelisted_caller(); + let contributor = account("contributor", 0, 0); + contribute_fund::(&contributor, fund_index); + frame_system::Module::::set_block_number(200u32.into()); + }: _(RawOrigin::Signed(caller), contributor.clone(), fund_index) + verify { + assert_last_event::(RawEvent::Withdrew(contributor, fund_index, T::MinContribution::get()).into()); + } + + // Worst case: Dissolve removes `RemoveKeysLimit` keys, and then finishes up the dissolution of the fund. + dissolve { + let fund_index = create_fund::(100u32.into()); + + // Dissolve will remove at most `RemoveKeysLimit` at once. + for i in 0 .. T::RemoveKeysLimit::get() { + contribute_fund::(&account("contributor", i, 0), fund_index); + } + + let caller: T::AccountId = whitelisted_caller(); + frame_system::Module::::set_block_number(T::RetirementPeriod::get().saturating_add(200u32.into())); + }: _(RawOrigin::Signed(caller.clone()), fund_index) + verify { + assert_last_event::(RawEvent::Dissolved(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 slot. + on_initialize { + // We test the complexity over different number of new raise + let n in 2 .. 100; + let end_block: T::BlockNumber = 100u32.into(); + + for i in 0 .. n { + let fund_index = create_fund::(end_block); + let contributor: T::AccountId = account("contributor", i, 0); + let contribution = T::MinContribution::get() * (i + 1).into(); + T::Currency::make_free_balance_be(&contributor, BalanceOf::::max_value()); + Crowdloan::::contribute(RawOrigin::Signed(contributor).into(), fund_index, contribution)?; + } + + let lease_period_index = end_block / T::LeasePeriod::get(); + Slots::::new_auction(RawOrigin::Root.into(), end_block, lease_period_index)?; + + assert_eq!(>::is_ending(end_block), Some(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::(RawEvent::HandleBidResult((n - 1).into(), Ok(())).into()); + } + } + + #[cfg(test)] + mod tests { + use super::*; + use crate::crowdloan::tests::{new_test_ext, Test}; + + #[test] + fn test_benchmarks() { + new_test_ext().execute_with(|| { + assert_ok!(test_benchmark_create::()); + assert_ok!(test_benchmark_contribute::()); + assert_ok!(test_benchmark_fix_deploy_data::()); + assert_ok!(test_benchmark_onboard::()); + assert_ok!(test_benchmark_begin_retirement::()); + assert_ok!(test_benchmark_withdraw::()); + assert_ok!(test_benchmark_dissolve::()); + assert_ok!(test_benchmark_on_initialize::()); + }); + } + } +} diff --git a/polkadot/runtime/common/src/lib.rs b/polkadot/runtime/common/src/lib.rs index c85e5224e0..45f1a8856b 100644 --- a/polkadot/runtime/common/src/lib.rs +++ b/polkadot/runtime/common/src/lib.rs @@ -21,7 +21,7 @@ pub mod claims; pub mod slot_range; pub mod slots; -pub mod crowdfund; +pub mod crowdloan; pub mod purchase; pub mod impls; pub mod paras_sudo_wrapper; diff --git a/polkadot/runtime/common/src/slots.rs b/polkadot/runtime/common/src/slots.rs index 66d4d9640f..9813d48581 100644 --- a/polkadot/runtime/common/src/slots.rs +++ b/polkadot/runtime/common/src/slots.rs @@ -579,9 +579,9 @@ impl Module { /// ending. An immediately subsequent call with the same argument will always return `None`. fn check_auction_end(now: T::BlockNumber) -> Option<(WinningData, LeasePeriodOf)> { if let Some((lease_period_index, early_end)) = >::get() { - if early_end + T::EndingPeriod::get() == now { + let ending_period = T::EndingPeriod::get(); + if early_end + ending_period == now { // Just ended! - let ending_period = T::EndingPeriod::get(); let offset = T::BlockNumber::decode(&mut T::Randomness::random_seed().as_ref()) .expect("secure hashes always bigger than block numbers; qed") % ending_period; let res = >::get(offset).unwrap_or_default();