crowdloan.rs 57.4 KB
Newer Older
Shawn Tabrizi's avatar
Shawn Tabrizi committed
1
// Copyright 2017-2020 Parity Technologies (UK) Ltd.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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 <http://www.gnu.org/licenses/>.

Shawn Tabrizi's avatar
Shawn Tabrizi committed
17
//! # Parachain Crowdloaning module
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
//!
//! 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.
//!
//! Contributing funds is permissionless. Each fund has a child-trie which stores all
//! contributors account IDs together with the amount they contributed; the root of this can then be
//! used by the parachain to allow contributors to prove that they made some particular contribution
//! to the project (e.g. to be rewarded through some token or badge). The trie is retained for later
//! (efficient) redistribution back to the contributors.
//!
//! Contributions must be of at least `MinContribution` (to account for the resources taken in
//! tracking contributions), and may never tally greater than the fund's `cap`, set and fixed at the
//! time of creation. The `create` call may be used to create a new fund. In order to do this, then
//! a deposit must be paid of the amount `SubmissionDeposit`. Substantial resources are taken on
//! the main trie in tracking a fund and this accounts for that.
//!
//! Funds may be set up during an auction period; their closing time is fixed at creation (as a
//! block number) and if the fund is not successful by the closing time, then it will become *retired*.
//! Funds may span multiple auctions, and even auctions that sell differing periods. However, for a
//! fund to be active in bidding for an auction, it *must* have had *at least one bid* since the end
//! of the last auction. Until a fund takes a further bid following the end of an auction, then it
//! will be inactive.
//!
//! Contributors may get a refund of their contributions from retired funds. After a period (`RetirementPeriod`)
//! the fund may be dissolved entirely. At this point any non-refunded contributions are considered
//! `orphaned` and are disposed of through the `OrphanedFunds` handler (which may e.g. place them
//! into the treasury).
//!
//! Funds may accept contributions at any point before their success or retirement. When a parachain
//! slot auction enters its ending period, then parachains will each place a bid; the bid will be
//! raised once per block if the parachain had additional funds contributed since the last bid.
//!
//! Funds may set their deploy data (the code hash and head data of their parachain) at any point.
//! It may only be done once and once set cannot be changed. Good procedure would be to set them
//! ahead of receiving any contributions in order that contributors may verify that their parachain
//! contains all expected functionality. However, this is not enforced and deploy data may happen
//! at any point, even after a slot has been successfully won or, indeed, never.
//!
//! Funds that are successful winners of a slot may have their slot claimed through the `onboard`
//! call. This may only be done once and must be after the deploy data has been fixed. Successful
//! funds remain tracked (in the `Funds` storage item and the associated child trie) as long as
//! the parachain remains active. Once it does not, it is up to the parachain to ensure that the
//! funds are returned to this module's fund sub-account in order that they be redistributed back to
//! contributors. *Retirement* may be initiated by any account (using the `begin_retirement` call)
//! once the parachain is removed from the its slot.
//!
//! @WARNING: For funds to be returned, it is imperative that this module's account is provided as
//! the offboarding account for the slot. In the case that a parachain supplemented these funds in
//! order to win a later auction, then it is the parachain's duty to ensure that the right amount of
//! funds ultimately end up in module's fund sub-account.

69
use frame_support::{
Shawn Tabrizi's avatar
Shawn Tabrizi committed
70
71
	decl_module, decl_storage, decl_event, decl_error, ensure,
	storage::child,
72
	traits::{
Shawn Tabrizi's avatar
Shawn Tabrizi committed
73
		Currency, Get, OnUnbalanced, ExistenceRequirement::AllowDeath
74
	},
75
};
76
use frame_system::ensure_signed;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
77
78
use sp_runtime::{ModuleId, DispatchResult,
	traits::{AccountIdConversion, Hash, Saturating, Zero, CheckedAdd, Bounded}
79
80
};
use crate::slots;
81
use parity_scale_codec::{Encode, Decode};
82
use sp_std::vec::Vec;
83
use primitives::v1::{Id as ParaId, HeadData};
84

85
pub type BalanceOf<T> =
86
	<<T as slots::Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
87
88
#[allow(dead_code)]
pub type NegativeImbalanceOf<T> =
89
	<<T as slots::Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;
90

91
92
pub trait Config: slots::Config {
	type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
93

Shawn Tabrizi's avatar
Shawn Tabrizi committed
94
	/// ModuleID for the crowdloan module. An appropriate value could be ```ModuleId(*b"py/cfund")```
95
96
	type ModuleId: Get<ModuleId>;

Shawn Tabrizi's avatar
Shawn Tabrizi committed
97
	/// The amount to be held on deposit by the owner of a crowdloan.
98
99
	type SubmissionDeposit: Get<BalanceOf<Self>>;

Shawn Tabrizi's avatar
Shawn Tabrizi committed
100
	/// The minimum amount that may be contributed into a crowdloan. Should almost certainly be at
101
102
103
	/// least ExistentialDeposit.
	type MinContribution: Get<BalanceOf<Self>>;

Shawn Tabrizi's avatar
Shawn Tabrizi committed
104
	/// The period of time (in blocks) after an unsuccessful crowdloan ending when
105
106
107
108
109
	/// contributors are able to withdraw their funds. After this period, their funds are lost.
	type RetirementPeriod: Get<Self::BlockNumber>;

	/// What to do with funds that were not withdrawn.
	type OrphanedFunds: OnUnbalanced<NegativeImbalanceOf<Self>>;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
110
111
112

	/// Max number of storage keys to remove per extrinsic call.
	type RemoveKeysLimit: Get<u32>;
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
}

/// Simple index for identifying a fund.
pub type FundIndex = u32;

#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
pub enum LastContribution<BlockNumber> {
	Never,
	PreEnding(slots::AuctionIndex),
	Ending(BlockNumber),
}

#[derive(Encode, Decode, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
128
129
130
struct DeployData<Hash> {
	code_hash: Hash,
	code_size: u32,
131
	initial_head_data: HeadData,
132
133
134
135
136
}

#[derive(Encode, Decode, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "std", derive(Debug))]
#[codec(dumb_trait_bound)]
137
pub struct FundInfo<AccountId, Balance, Hash, BlockNumber> {
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
	/// The parachain that this fund has funded, if there is one. As long as this is `Some`, then
	/// the funds may not be withdrawn and the fund cannot be dissolved.
	parachain: Option<ParaId>,
	/// The owning account who placed the deposit.
	owner: AccountId,
	/// The amount of deposit placed.
	deposit: Balance,
	/// The total amount raised.
	raised: Balance,
	/// Block number after which the funding must have succeeded. If not successful at this number
	/// then everyone may withdraw their funds.
	end: BlockNumber,
	/// A hard-cap on the amount that may be contributed.
	cap: Balance,
	/// The most recent block that this had a contribution. Determines if we make a bid or not.
	/// If this is `Never`, this fund has never received a contribution.
	/// If this is `PreEnding(n)`, this fund received a contribution sometime in auction
	/// number `n` before the ending period.
	/// If this is `Ending(n)`, this fund received a contribution during the current ending period,
	/// where `n` is how far into the ending period the contribution was made.
	last_contribution: LastContribution<BlockNumber>,
	/// First slot in range to bid on; it's actually a LeasePeriod, but that's the same type as
	/// BlockNumber.
	first_slot: BlockNumber,
	/// Last slot in range to bid on; it's actually a LeasePeriod, but that's the same type as
	/// BlockNumber.
	last_slot: BlockNumber,
	/// The deployment data associated with this fund, if any. Once set it may not be reset. First
166
167
	/// is the code hash, second is the code size, third is the initial head data.
	deploy_data: Option<DeployData<Hash>>,
168
169
170
}

decl_storage! {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
171
	trait Store for Module<T: Config> as Crowdloan {
172
		/// Info on all of the funds.
173
		Funds get(fn funds):
174
			map hasher(twox_64_concat) FundIndex
Gavin Wood's avatar
Gavin Wood committed
175
			=> Option<FundInfo<T::AccountId, BalanceOf<T>, T::Hash, T::BlockNumber>>;
176
177

		/// The total number of funds that have so far been allocated.
178
		FundCount get(fn fund_count): FundIndex;
179
180
181

		/// The funds that have had additional contributions during the last block. This is used
		/// in order to determine which funds should submit new or updated bids.
182
		NewRaise get(fn new_raise): Vec<FundIndex>;
183
184

		/// The number of auctions that have entered into their ending period so far.
185
		EndingsCount get(fn endings_count): slots::AuctionIndex;
186
187
188
189
190
	}
}

decl_event! {
	pub enum Event<T> where
191
		<T as frame_system::Config>::AccountId,
192
193
		Balance = BalanceOf<T>,
	{
Shawn Tabrizi's avatar
Shawn Tabrizi committed
194
		/// Create a new crowdloaning campaign. [fund_index]
195
		Created(FundIndex),
196
		/// Contributed to a crowd sale. [who, fund_index, amount]
197
		Contributed(AccountId, FundIndex, Balance),
198
		/// Withdrew full balance of a contributor. [who, fund_index, amount]
199
		Withdrew(AccountId, FundIndex, Balance),
200
		/// Fund is placed into retirement. [fund_index]
201
		Retiring(FundIndex),
Shawn Tabrizi's avatar
Shawn Tabrizi committed
202
203
204
		/// Fund is partially dissolved, i.e. there are some left over child
		/// keys that still need to be killed. [fund_index]
		PartiallyDissolved(FundIndex),
205
		/// Fund is dissolved. [fund_index]
206
		Dissolved(FundIndex),
207
		/// The deploy data of the funded parachain is setted. [fund_index]
208
		DeployDataFixed(FundIndex),
209
		/// Onboarding process for a winning parachain fund is completed. [find_index, parachain_id]
210
		Onboarded(FundIndex, ParaId),
Shawn Tabrizi's avatar
Shawn Tabrizi committed
211
212
		/// The result of trying to submit a new bid to the Slots pallet.
		HandleBidResult(FundIndex, DispatchResult),
213
214
215
	}
}

216
decl_error! {
217
	pub enum Error for Module<T: Config> {
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
		/// Last slot must be greater than first slot.
		LastSlotBeforeFirstSlot,
		/// The last slot cannot be more then 3 slots after the first slot.
		LastSlotTooFarInFuture,
		/// The campaign ends before the current block number. The end must be in the future.
		CannotEndInPast,
		/// There was an overflow.
		Overflow,
		/// The contribution was below the minimum, `MinContribution`.
		ContributionTooSmall,
		/// Invalid fund index.
		InvalidFundIndex,
		/// Contributions exceed maximum amount.
		CapExceeded,
		/// The contribution period has already ended.
		ContributionPeriodOver,
		/// The origin of this call is invalid.
		InvalidOrigin,
		/// Deployment data for a fund can only be set once. The deployment data for this fund
		/// already exists.
		ExistingDeployData,
		/// Deployment data has not been set for this fund.
		UnsetDeployData,
		/// This fund has already been onboarded.
		AlreadyOnboard,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
243
		/// This crowdloan does not correspond to a parachain.
244
245
246
247
248
249
250
		NotParachain,
		/// This parachain still has its deposit. Implies that it has already been offboarded.
		ParaHasDeposit,
		/// Funds have not yet been returned.
		FundsNotReturned,
		/// Fund has not yet retired.
		FundNotRetired,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
251
		/// The crowdloan has not yet ended.
252
		FundNotEnded,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
253
		/// There are no contributions stored in this crowdloan.
254
		NoContributions,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
255
		/// This crowdloan has an active parachain and cannot be dissolved.
256
257
258
259
260
261
		HasActiveParachain,
		/// The retirement period has not ended.
		InRetirementPeriod,
	}
}

262
decl_module! {
263
	pub struct Module<T: Config> for enum Call where origin: T::Origin {
264
265
		type Error = Error<T>;

266
267
		const ModuleId: ModuleId = T::ModuleId::get();

268
		fn deposit_event() = default;
269

Shawn Tabrizi's avatar
Shawn Tabrizi committed
270
		/// Create a new crowdloaning campaign for a parachain slot deposit for the current auction.
271
		#[weight = 100_000_000]
272
273
274
275
276
277
278
279
		fn create(origin,
			#[compact] cap: BalanceOf<T>,
			#[compact] first_slot: T::BlockNumber,
			#[compact] last_slot: T::BlockNumber,
			#[compact] end: T::BlockNumber
		) {
			let owner = ensure_signed(origin)?;

280
			ensure!(first_slot < last_slot, Error::<T>::LastSlotBeforeFirstSlot);
281
			ensure!(last_slot <= first_slot + 3u32.into(), Error::<T>::LastSlotTooFarInFuture);
282
			ensure!(end > <frame_system::Module<T>>::block_number(), Error::<T>::CannotEndInPast);
283
284

			let index = FundCount::get();
285
			let next_index = index.checked_add(1).ok_or(Error::<T>::Overflow)?;
286

Shawn Tabrizi's avatar
Shawn Tabrizi committed
287
288
289
			let deposit = T::SubmissionDeposit::get();
			T::Currency::transfer(&owner, &Self::fund_account_id(index), deposit, AllowDeath)?;
			FundCount::put(next_index);
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305

			<Funds<T>>::insert(index, FundInfo {
				parachain: None,
				owner,
				deposit,
				raised: Zero::zero(),
				end,
				cap,
				last_contribution: LastContribution::Never,
				first_slot,
				last_slot,
				deploy_data: None,
			});

			Self::deposit_event(RawEvent::Created(index));
		}
306

307
308
		/// Contribute to a crowd sale. This will transfer some balance over to fund a parachain
		/// slot. It will be withdrawable in two instances: the parachain becomes retired; or the
Gavin Wood's avatar
Gavin Wood committed
309
		/// slot is unable to be purchased and the timeout expires.
310
		#[weight = 0]
311
312
313
		fn contribute(origin, #[compact] index: FundIndex, #[compact] value: BalanceOf<T>) {
			let who = ensure_signed(origin)?;

314
315
316
317
			ensure!(value >= T::MinContribution::get(), Error::<T>::ContributionTooSmall);
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidFundIndex)?;
			fund.raised  = fund.raised.checked_add(&value).ok_or(Error::<T>::Overflow)?;
			ensure!(fund.raised <= fund.cap, Error::<T>::CapExceeded);
318

Shawn Tabrizi's avatar
Shawn Tabrizi committed
319
			// Make sure crowdloan has not ended
320
			let now = <frame_system::Module<T>>::block_number();
321
			ensure!(fund.end > now, Error::<T>::ContributionPeriodOver);
322

323
			T::Currency::transfer(&who, &Self::fund_account_id(index), value, AllowDeath)?;
324
325
326
327
328
329
330
331
332
333
334
335

			let balance = Self::contribution_get(index, &who);
			let balance = balance.saturating_add(value);
			Self::contribution_put(index, &who, &balance);

			if <slots::Module<T>>::is_ending(now).is_some() {
				match fund.last_contribution {
					// In ending period; must ensure that we are in NewRaise.
					LastContribution::Ending(n) if n == now => {
						// do nothing - already in NewRaise
					}
					_ => {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
336
						NewRaise::append(index);
337
338
339
340
341
342
343
344
345
346
347
348
349
350
						fund.last_contribution = LastContribution::Ending(now);
					}
				}
			} else {
				let endings_count = Self::endings_count();
				match fund.last_contribution {
					LastContribution::PreEnding(a) if a == endings_count => {
						// Not in ending period and no auctions have ended ending since our
						// previous bid which was also not in an ending period.
						// `NewRaise` will contain our ID still: Do nothing.
					}
					_ => {
						// Not in ending period; but an auction has been ending since our previous
						// bid, or we never had one to begin with. Add bid.
Shawn Tabrizi's avatar
Shawn Tabrizi committed
351
						NewRaise::append(index);
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
						fund.last_contribution = LastContribution::PreEnding(endings_count);
					}
				}
			}

			<Funds<T>>::insert(index, &fund);

			Self::deposit_event(RawEvent::Contributed(who, index, value));
		}

		/// Set the deploy data of the funded parachain if not already set. Once set, this cannot
		/// be changed again.
		///
		/// - `origin` must be the fund owner.
		/// - `index` is the fund index that `origin` owns and whose deploy data will be set.
		/// - `code_hash` is the hash of the parachain's Wasm validation function.
		/// - `initial_head_data` is the parachain's initial head data.
369
		#[weight = 0]
370
371
372
		fn fix_deploy_data(origin,
			#[compact] index: FundIndex,
			code_hash: T::Hash,
373
			code_size: u32,
374
			initial_head_data: HeadData,
375
376
377
		) {
			let who = ensure_signed(origin)?;

378
379
380
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidFundIndex)?;
			ensure!(fund.owner == who, Error::<T>::InvalidOrigin); // must be fund owner
			ensure!(fund.deploy_data.is_none(), Error::<T>::ExistingDeployData);
381

382
			fund.deploy_data = Some(DeployData { code_hash, code_size, initial_head_data });
383
384
385
386
387

			<Funds<T>>::insert(index, &fund);

			Self::deposit_event(RawEvent::DeployDataFixed(index));
		}
388

389
390
391
392
393
394
		/// Complete onboarding process for a winning parachain fund. This can be called once by
		/// any origin once a fund wins a slot and the fund has set its deploy data (using
		/// `fix_deploy_data`).
		///
		/// - `index` is the fund index that `origin` owns and whose deploy data will be set.
		/// - `para_id` is the parachain index that this fund won.
395
		#[weight = 0]
396
397
		fn onboard(origin,
			#[compact] index: FundIndex,
398
			#[compact] para_id: ParaId
399
		) {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
400
			ensure_signed(origin)?;
401

402
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidFundIndex)?;
403
404
			let DeployData { code_hash, code_size, initial_head_data }
				= fund.clone().deploy_data.ok_or(Error::<T>::UnsetDeployData)?;
405
			ensure!(fund.parachain.is_none(), Error::<T>::AlreadyOnboard);
406
407
			fund.parachain = Some(para_id);

408
			let fund_origin = frame_system::RawOrigin::Signed(Self::fund_account_id(index)).into();
409
410
411
412
413
414
415
416
			<slots::Module<T>>::fix_deploy_data(
				fund_origin,
				index,
				para_id,
				code_hash,
				code_size,
				initial_head_data,
			)?;
417
418
419
420
421

			<Funds<T>>::insert(index, &fund);

			Self::deposit_event(RawEvent::Onboarded(index, para_id));
		}
422

423
		/// Note that a successful fund has lost its parachain slot, and place it into retirement.
424
		#[weight = 0]
425
		fn begin_retirement(origin, #[compact] index: FundIndex) {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
426
			ensure_signed(origin)?;
427

428
429
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidFundIndex)?;
			let parachain_id = fund.parachain.take().ok_or(Error::<T>::NotParachain)?;
430
			// No deposit information implies the parachain was off-boarded
431
			ensure!(<slots::Module<T>>::deposits(parachain_id).len() == 0, Error::<T>::ParaHasDeposit);
432
433
			let account = Self::fund_account_id(index);
			// Funds should be returned at the end of off-boarding
434
			ensure!(T::Currency::free_balance(&account) >= fund.raised, Error::<T>::FundsNotReturned);
435
436

			// This fund just ended. Withdrawal period begins.
437
			let now = <frame_system::Module<T>>::block_number();
438
439
440
441
442
443
444
445
			fund.end = now;

			<Funds<T>>::insert(index, &fund);

			Self::deposit_event(RawEvent::Retiring(index));
		}

		/// Withdraw full balance of a contributor to an unsuccessful or off-boarded fund.
446
		#[weight = 0]
Shawn Tabrizi's avatar
Shawn Tabrizi committed
447
448
		fn withdraw(origin, who: T::AccountId, #[compact] index: FundIndex) {
			ensure_signed(origin)?;
449

450
451
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidFundIndex)?;
			ensure!(fund.parachain.is_none(), Error::<T>::FundNotRetired);
452
			let now = <frame_system::Module<T>>::block_number();
453
454

			// `fund.end` can represent the end of a failed crowdsale or the beginning of retirement
455
			ensure!(now >= fund.end, Error::<T>::FundNotEnded);
456
457

			let balance = Self::contribution_get(index, &who);
458
			ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
459
460

			// Avoid using transfer to ensure we don't pay any fees.
Shawn Tabrizi's avatar
Shawn Tabrizi committed
461
462
			let fund_account = Self::fund_account_id(index);
			T::Currency::transfer(&fund_account, &who, balance, AllowDeath)?;
463
464
465
466
467
468
469
470

			Self::contribution_kill(index, &who);
			fund.raised = fund.raised.saturating_sub(balance);

			<Funds<T>>::insert(index, &fund);

			Self::deposit_event(RawEvent::Withdrew(who, index, balance));
		}
471

472
473
474
		/// Remove a fund after either: it was unsuccessful and it timed out; or it was successful
		/// but it has been retired from its parachain slot. This places any deposits that were not
		/// withdrawn into the treasury.
475
		#[weight = 0]
476
		fn dissolve(origin, #[compact] index: FundIndex) {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
477
			ensure_signed(origin)?;
478

479
480
			let fund = Self::funds(index).ok_or(Error::<T>::InvalidFundIndex)?;
			ensure!(fund.parachain.is_none(), Error::<T>::HasActiveParachain);
481
			let now = <frame_system::Module<T>>::block_number();
482
483
			ensure!(
				now >= fund.end.saturating_add(T::RetirementPeriod::get()),
484
				Error::<T>::InRetirementPeriod
485
			);
486

Shawn Tabrizi's avatar
Shawn Tabrizi committed
487
488
489
490
491
			// 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)?;
492

Shawn Tabrizi's avatar
Shawn Tabrizi committed
493
494
495
					// Remove all other balance from the account into orphaned funds.
					let (imbalance, _) = T::Currency::slash(&account, BalanceOf::<T>::max_value());
					T::OrphanedFunds::on_unbalanced(imbalance);
496

Shawn Tabrizi's avatar
Shawn Tabrizi committed
497
					<Funds<T>>::remove(index);
498

Shawn Tabrizi's avatar
Shawn Tabrizi committed
499
500
501
502
503
504
					Self::deposit_event(RawEvent::Dissolved(index));
				},
				child::KillOutcome::SomeRemaining => {
					Self::deposit_event(RawEvent::PartiallyDissolved(index));
				}
			}
505
		}
506

Shawn Tabrizi's avatar
Shawn Tabrizi committed
507
		fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight {
508
509
510
511
512
513
514
515
516
517
518
519
520
521
			if let Some(n) = <slots::Module<T>>::is_ending(n) {
				let auction_index = <slots::Module<T>>::auction_counter();
				if n.is_zero() {
					// first block of ending period.
					EndingsCount::mutate(|c| *c += 1);
				}
				for (fund, index) in NewRaise::take().into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) {
					let bidder = slots::Bidder::New(slots::NewBidder {
						who: Self::fund_account_id(index),
						/// FundIndex and slots::SubId happen to be the same type (u32). If this
						/// ever changes, then some sort of conversion will be needed here.
						sub: index,
					});

Shawn Tabrizi's avatar
Shawn Tabrizi committed
522
523
524
					// 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 = <slots::Module<T>>::handle_bid(
525
526
527
528
529
530
						bidder,
						auction_index,
						fund.first_slot,
						fund.last_slot,
						fund.raised,
					);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
531
532

					Self::deposit_event(RawEvent::HandleBidResult(index, result));
533
534
				}
			}
Shawn Tabrizi's avatar
Shawn Tabrizi committed
535
536

			0
537
538
539
540
		}
	}
}

541
impl<T: Config> Module<T> {
542
543
544
545
546
	/// The account ID of the fund pot.
	///
	/// This actually does computation. If you need to keep using it, then make sure you cache the
	/// value and only call this once.
	pub fn fund_account_id(index: FundIndex) -> T::AccountId {
547
		T::ModuleId::get().into_sub_account(index)
548
549
	}

550
	pub fn id_from_index(index: FundIndex) -> child::ChildInfo {
551
		let mut buf = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
552
		buf.extend_from_slice(b"crowdloan");
553
		buf.extend_from_slice(&index.to_le_bytes()[..]);
554
		child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref())
555
556
	}

557
	pub fn contribution_put(index: FundIndex, who: &T::AccountId, balance: &BalanceOf<T>) {
558
		who.using_encoded(|b| child::put(&Self::id_from_index(index), b, balance));
559
560
561
	}

	pub fn contribution_get(index: FundIndex, who: &T::AccountId) -> BalanceOf<T> {
562
		who.using_encoded(|b| child::get_or_default::<BalanceOf<T>>(
563
			&Self::id_from_index(index),
564
565
			b,
		))
566
567
568
	}

	pub fn contribution_kill(index: FundIndex, who: &T::AccountId) {
569
		who.using_encoded(|b| child::kill(&Self::id_from_index(index), b));
570
571
	}

Shawn Tabrizi's avatar
Shawn Tabrizi committed
572
573
	pub fn crowdloan_kill(index: FundIndex) -> child::KillOutcome {
		child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get()))
574
575
576
577
578
579
580
581
	}
}

#[cfg(test)]
mod tests {
	use super::*;

	use std::{collections::HashMap, cell::RefCell};
582
	use frame_support::{
Shawn Tabrizi's avatar
Shawn Tabrizi committed
583
		impl_outer_origin, impl_outer_event, assert_ok, assert_noop, parameter_types,
584
585
		traits::{OnInitialize, OnFinalize},
	};
586
	use sp_core::H256;
587
	use primitives::v1::{Id as ParaId, ValidationCode};
588
589
	// 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.
590
	use sp_runtime::{
Shawn Tabrizi's avatar
Shawn Tabrizi committed
591
		Permill, testing::Header,
592
		traits::{BlakeTwo256, IdentityLookup},
593
	};
594
	use crate::slots::Registrar;
595
596

	impl_outer_origin! {
597
		pub enum Origin for Test {}
598
599
	}

Shawn Tabrizi's avatar
Shawn Tabrizi committed
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
	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<T>,
			pallet_balances<T>,
			pallet_treasury<T>,
			runtime_common_slots<T>,
			runtime_common_crowdloan<T>,
		}
	}

618
619
620
621
622
623
624
625
	// 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.
	#[derive(Clone, Eq, PartialEq)]
	pub struct Test;
	parameter_types! {
		pub const BlockHashCount: u32 = 250;
	}
626

627
	impl frame_system::Config for Test {
628
		type BaseCallFilter = ();
629
630
631
		type BlockWeights = ();
		type BlockLength = ();
		type DbWeight = ();
632
633
634
635
636
637
638
639
640
		type Origin = Origin;
		type Call = ();
		type Index = u64;
		type BlockNumber = u64;
		type Hash = H256;
		type Hashing = BlakeTwo256;
		type AccountId = u64;
		type Lookup = IdentityLookup<Self::AccountId>;
		type Header = Header;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
641
		type Event = Event;
642
643
		type BlockHashCount = BlockHashCount;
		type Version = ();
644
		type PalletInfo = ();
645
		type AccountData = pallet_balances::AccountData<u64>;
Gavin Wood's avatar
Gavin Wood committed
646
		type OnNewAccount = ();
647
		type OnKilledAccount = ();
648
		type SystemWeightInfo = ();
649
		type SS58Prefix = ();
650
651
	}
	parameter_types! {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
652
		pub const ExistentialDeposit: u64 = 1;
653
	}
654
	impl pallet_balances::Config for Test {
655
		type Balance = u64;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
656
		type Event = Event;
657
		type DustRemoval = ();
658
		type ExistentialDeposit = ExistentialDeposit;
659
		type AccountStore = System;
660
		type MaxLocks = ();
661
		type WeightInfo = ();
662
663
664
665
666
667
668
	}

	parameter_types! {
		pub const ProposalBond: Permill = Permill::from_percent(5);
		pub const ProposalBondMinimum: u64 = 1;
		pub const SpendPeriod: u64 = 2;
		pub const Burn: Permill = Permill::from_percent(50);
669
		pub const TreasuryModuleId: ModuleId = ModuleId(*b"py/trsry");
670
	}
671
	impl pallet_treasury::Config for Test {
672
673
674
		type Currency = pallet_balances::Module<Test>;
		type ApproveOrigin = frame_system::EnsureRoot<u64>;
		type RejectOrigin = frame_system::EnsureRoot<u64>;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
675
		type Event = Event;
676
		type OnSlash = ();
677
678
679
680
		type ProposalBond = ProposalBond;
		type ProposalBondMinimum = ProposalBondMinimum;
		type SpendPeriod = SpendPeriod;
		type Burn = Burn;
681
		type BurnDestination = ();
682
		type ModuleId = TreasuryModuleId;
683
		type SpendFunds = ();
684
		type WeightInfo = ();
685
686
687
688
689
	}

	thread_local! {
		pub static PARACHAIN_COUNT: RefCell<u32> = RefCell::new(0);
		pub static PARACHAINS:
690
			RefCell<HashMap<u32, (ValidationCode, HeadData)>> = RefCell::new(HashMap::new());
691
692
	}

693
694
695
	const MAX_CODE_SIZE: u32 = 100;
	const MAX_HEAD_DATA_SIZE: u32 = 10;

696
	pub struct TestParachains;
697
698
	impl Registrar<u64> for TestParachains {
		fn new_id() -> ParaId {
699
700
701
702
703
			PARACHAIN_COUNT.with(|p| {
				*p.borrow_mut() += 1;
				(*p.borrow() - 1).into()
			})
		}
704

705
706
707
708
709
710
711
712
		fn head_data_size_allowed(head_data_size: u32) -> bool {
			head_data_size <= MAX_HEAD_DATA_SIZE
		}

		fn code_size_allowed(code_size: u32) -> bool {
			code_size <= MAX_CODE_SIZE
		}

713
714
		fn register_para(
			id: ParaId,
715
			_parachain: bool,
716
717
			code: ValidationCode,
			initial_head_data: HeadData,
718
		) -> DispatchResult {
719
			PARACHAINS.with(|p| {
720
				if p.borrow().contains_key(&id.into()) {
721
722
					panic!("ID already exists")
				}
723
				p.borrow_mut().insert(id.into(), (code, initial_head_data));
724
725
726
				Ok(())
			})
		}
727
728

		fn deregister_para(id: ParaId) -> DispatchResult {
729
			PARACHAINS.with(|p| {
730
				if !p.borrow().contains_key(&id.into()) {
731
732
					panic!("ID doesn't exist")
				}
733
				p.borrow_mut().remove(&id.into());
734
735
736
737
738
739
740
741
742
				Ok(())
			})
		}
	}

	parameter_types!{
		pub const LeasePeriod: u64 = 10;
		pub const EndingPeriod: u64 = 3;
	}
743
	impl slots::Config for Test {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
744
		type Event = Event;
745
746
747
748
		type Currency = Balances;
		type Parachains = TestParachains;
		type LeasePeriod = LeasePeriod;
		type EndingPeriod = EndingPeriod;
749
		type Randomness = RandomnessCollectiveFlip;
750
751
752
753
754
	}
	parameter_types! {
		pub const SubmissionDeposit: u64 = 1;
		pub const MinContribution: u64 = 10;
		pub const RetirementPeriod: u64 = 5;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
755
756
		pub const CrowdloanModuleId: ModuleId = ModuleId(*b"py/cfund");
		pub const RemoveKeysLimit: u32 = 10;
757
	}
758
	impl Config for Test {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
759
		type Event = Event;
760
761
762
763
		type SubmissionDeposit = SubmissionDeposit;
		type MinContribution = MinContribution;
		type RetirementPeriod = RetirementPeriod;
		type OrphanedFunds = Treasury;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
764
765
		type ModuleId = CrowdloanModuleId;
		type RemoveKeysLimit = RemoveKeysLimit;
766
767
	}

768
769
	type System = frame_system::Module<Test>;
	type Balances = pallet_balances::Module<Test>;
770
	type Slots = slots::Module<Test>;
771
	type Treasury = pallet_treasury::Module<Test>;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
772
	type Crowdloan = Module<Test>;
773
774
	type RandomnessCollectiveFlip = pallet_randomness_collective_flip::Module<Test>;
	use pallet_balances::Error as BalancesError;
775
	use slots::Error as SlotsError;
776
777
778

	// This function basically just builds a genesis storage key/value store according to
	// our desired mockup.
Shawn Tabrizi's avatar
Shawn Tabrizi committed
779
	pub fn new_test_ext() -> sp_io::TestExternalities {
780
781
		let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
		pallet_balances::GenesisConfig::<Test>{
782
783
784
785
786
787
788
			balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
		}.assimilate_storage(&mut t).unwrap();
		t.into()
	}

	fn run_to_block(n: u64) {
		while System::block_number() < n {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
789
			Crowdloan::on_finalize(System::block_number());
790
791
792
793
794
795
796
797
			Treasury::on_finalize(System::block_number());
			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());
798
			Treasury::on_initialize(System::block_number());
Shawn Tabrizi's avatar
Shawn Tabrizi committed
799
			Crowdloan::on_initialize(System::block_number());
800
801
802
803
804
		}
	}

	#[test]
	fn basic_setup_works() {
805
		new_test_ext().execute_with(|| {
806
			assert_eq!(System::block_number(), 0);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
807
808
			assert_eq!(Crowdloan::fund_count(), 0);
			assert_eq!(Crowdloan::funds(0), None);
809
			let empty: Vec<FundIndex> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
810
811
812
			assert_eq!(Crowdloan::new_raise(), empty);
			assert_eq!(Crowdloan::contribution_get(0, &1), 0);
			assert_eq!(Crowdloan::endings_count(), 0);
813
814
815
816
817
		});
	}

	#[test]
	fn create_works() {
818
		new_test_ext().execute_with(|| {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
819
820
821
			// Now try to create a crowdloan campaign
			assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9));
			assert_eq!(Crowdloan::fund_count(), 1);
822
823
824
825
826
827
828
829
830
831
832
833
834
835
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
				parachain: None,
				owner: 1,
				deposit: 1,
				raised: 0,
				// 5 blocks length + 3 block ending period + 1 starting block
				end: 9,
				cap: 1000,
				last_contribution: LastContribution::Never,
				first_slot: 1,
				last_slot: 4,
				deploy_data: None,
			};
Shawn Tabrizi's avatar
Shawn Tabrizi committed
836
			assert_eq!(Crowdloan::funds(0), Some(fund_info));
837
838
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
839
840
			// Deposit is placed in crowdloan free balance
			assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1);
841
842
			// No new raise until first contribution
			let empty: Vec<FundIndex> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
843
			assert_eq!(Crowdloan::new_raise(), empty);
844
845
846
847
848
		});
	}

	#[test]
	fn create_handles_basic_errors() {
849
		new_test_ext().execute_with(|| {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
850
			// Cannot create a crowdloan with bad slots
851
			assert_noop!(
Shawn Tabrizi's avatar
Shawn Tabrizi committed
852
				Crowdloan::create(Origin::signed(1), 1000, 4, 1, 9),
853
854
855
				Error::<Test>::LastSlotBeforeFirstSlot
			);
			assert_noop!(
Shawn Tabrizi's avatar
Shawn Tabrizi committed
856
				Crowdloan::create(Origin::signed(1), 1000, 1, 5, 9),
857
858
				Error::<Test>::LastSlotTooFarInFuture
			);
859

Shawn Tabrizi's avatar
Shawn Tabrizi committed
860
			// Cannot create a crowdloan without some deposit funds
861
			assert_noop!(
Shawn Tabrizi's avatar
Shawn Tabrizi committed
862
				Crowdloan::create(Origin::signed(1337), 1000, 1, 3, 9),
863
864
				BalancesError::<Test, _>::InsufficientBalance
			);
865
866
867
868
869
		});
	}

	#[test]
	fn contribute_works() {
870
		new_test_ext().execute_with(|| {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
871
872
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9));
873
			assert_eq!(Balances::free_balance(1), 999);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
874
			assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(0)), 1);
875
876

			// No contributions yet
Shawn Tabrizi's avatar
Shawn Tabrizi committed
877
			assert_eq!(Crowdloan::contribution_get(0, &1), 0);
878

Shawn Tabrizi's avatar
Shawn Tabrizi committed
879
880
			// User 1 contributes to their own crowdloan
			assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 49));
881
			// User 1 has spent some funds to do this, transfer fees **are** taken
Gavin Wood's avatar
Gavin Wood committed
882
			assert_eq!(Balances::free_balance(1), 950);
883
			// Contributions are stored in the trie
Shawn Tabrizi's avatar
Shawn Tabrizi committed
884
885
886
887
888
			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]);
889

Shawn Tabrizi's avatar
Shawn Tabrizi committed
890
			let fund = Crowdloan::funds(0).unwrap();
891
892
893
894
895
896
897
898
899

			// Last contribution time recorded
			assert_eq!(fund.last_contribution, LastContribution::PreEnding(0));
			assert_eq!(fund.raised, 49);
		});
	}

	#[test]
	fn contribute_handles_basic_errors() {
900
		new_test_ext().execute_with(|| {
901
			// Cannot contribute to non-existing fund
Shawn Tabrizi's avatar
Shawn Tabrizi committed
902
			assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49), Error::<Test>::InvalidFundIndex);
903
			// Cannot contribute below minimum contribution
Shawn Tabrizi's avatar
Shawn Tabrizi committed
904
			assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 9), Error::<Test>::ContributionTooSmall);
905

Shawn Tabrizi's avatar
Shawn Tabrizi committed
906
907
908
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9));
			assert_ok!(Crowdloan::contribute(Origin::signed(1), 0, 101));
909
910

			// Cannot contribute past the limit
Shawn Tabrizi's avatar
Shawn Tabrizi committed
911
			assert_noop!(Crowdloan::contribute(Origin::signed(2), 0, 900), Error::<Test>::CapExceeded);
912
913
914
915
916

			// Move past end date
			run_to_block(10);

			// Cannot contribute to ended fund
Shawn Tabrizi's avatar
Shawn Tabrizi committed
917
			assert_noop!(Crowdloan::contribute(Origin::signed(1), 0, 49), Error::<Test>::ContributionPeriodOver);
918
919
920
921
922
		});
	}

	#[test]
	fn fix_deploy_data_works() {
923
		new_test_ext().execute_with(|| {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
924
925
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9));
926
927
928
			assert_eq!(Balances::free_balance(1), 999);

			// Add deploy data
Shawn Tabrizi's avatar
Shawn Tabrizi committed
929
			assert_ok!(Crowdloan::fix_deploy_data(
930
931
				Origin::signed(1),
				0,
932
				<Test as frame_system::Config>::Hash::default(),
933
				0,
934
				vec![0].into()
935
936
			));

Shawn Tabrizi's avatar
Shawn Tabrizi committed
937
			let fund = Crowdloan::funds(0).unwrap();
938
939

			// Confirm deploy data is stored correctly
940
941
942
			assert_eq!(
				fund.deploy_data,
				Some(DeployData {
943
					code_hash: <Test as frame_system::Config>::Hash::default(),
944
					code_size: 0,
945
					initial_head_data: vec![0].into(),
946
947
				}),
			);
948
949
950
951
952
		});
	}

	#[test]
	fn fix_deploy_data_handles_basic_errors() {
953
		new_test_ext().execute_with(|| {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
954
955
			// Set up a crowdloan
			assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9));
956
957
958
			assert_eq!(Balances::free_balance(1), 999);

			// Cannot set deploy data by non-owner
Shawn Tabrizi's avatar
Shawn Tabrizi committed
959
			assert_noop!(Crowdloan::fix_deploy_data(
960
961
				Origin::signed(2),
				0,
962
				<Test as frame_system::Config>::Hash::default(),
963
				0,
964
				vec![0].into()),
965
				Error::<Test>::InvalidOrigin
966
967
968
			);

			// Cannot set deploy data to an invalid index
Shawn Tabrizi's avatar
Shawn Tabrizi committed
969
			assert_noop!(Crowdloan::fix_deploy_data(
970
971
				Origin::signed(1),
				1,
972
				<Test as frame_system::Config>::Hash::default(),
973
				0,
974
				vec![0].into()),
975
				Error::<Test>::InvalidFundIndex
976
977
978
			);

			// Cannot set deploy data after it already has been set
Shawn Tabrizi's avatar
Shawn Tabrizi committed
979
			assert_ok!(Crowdloan::fix_deploy_data(
980
981
				Origin::signed(1),
				0,
982
				<Test as frame_system::Config>::Hash::default(),
983
				0,
984
				vec![0].into(),
985
986
			));

Shawn Tabrizi's avatar
Shawn Tabrizi committed
987
			assert_noop!(Crowdloan::fix_deploy_data(
988
989
				Origin::signed(1),
				0,
990
				<Test as frame_system::Config>::Hash::default(),
991
				0,
992
				vec![1].into()),
993
				Error::<Test>::ExistingDeployData
994
995
996
997
998
999
			);
		});
	}

	#[test]
	fn onboard_works() {
1000
		new_test_ext().execute_with(|| {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
1001
			// Set up a crowdloan
1002
			assert_ok!(Slots::new_auction(Origin::root(), 5, 1));
Shawn Tabrizi's avatar
Shawn Tabrizi committed
1003
			assert_ok!(Crowdloan::create(Origin::signed(1), 1000, 1, 4, 9));
1004
1005
1006
			assert_eq!(Balances::free_balance(1), 999);

			// Add deploy data
Shawn Tabrizi's avatar
Shawn Tabrizi committed
1007
			assert_ok!(Crowdloan::fix_deploy_data(
1008
1009
				Origin::signed(1),
				0,
1010
				<Test as frame_system::Config>::Hash::default(),
1011
				0,
1012
				vec![0].into(),
1013
1014
			));

Shawn Tabrizi's avatar
Shawn Tabrizi committed
1015
1016
			// Fund crowdloan
			assert_ok!(Crowdloan::contribute(Origin::signed(2), 0, 1000));
1017
1018
1019
1020

			run_to_block(10);

			// Endings count incremented
Shawn Tabrizi's avatar
Shawn Tabrizi committed
1021
			assert_eq!(Crowdloan::endings_count(), 1);
1022

Shawn Tabrizi's avatar
Shawn Tabrizi committed
1023
1024
			// Onboard crowdloan
			assert_ok!(Crowdloan::onboard(Origin::signed(1), 0, 0.into()));
1025

Shawn Tabrizi's avatar
Shawn Tabrizi committed
1026
1027
			let fund = Crowdloan::funds(0).unwrap();