crowdloan.rs 54.6 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::{
73
		Currency, ReservableCurrency, Get, OnUnbalanced, ExistenceRequirement::AllowDeath
74
	},
75
	pallet_prelude::{Weight, DispatchResultWithPostInfo},
76
};
77
use frame_system::{ensure_signed, ensure_root};
78
use sp_runtime::{
79
	ModuleId, DispatchResult, RuntimeDebug, MultiSignature, MultiSigner,
80
81
82
	traits::{
		AccountIdConversion, Hash, Saturating, Zero, CheckedAdd, Bounded, Verify, IdentifyAccount,
	},
83
};
84
use crate::traits::{Registrar, Auctioneer};
85
use parity_scale_codec::{Encode, Decode};
86
use sp_std::vec::Vec;
87
88
89
90
91
use primitives::v1::Id as ParaId;

type CurrencyOf<T> = <<T as Config>::Auctioneer as Auctioneer>::Currency;
type LeasePeriodOf<T> = <<T as Config>::Auctioneer as Auctioneer>::LeasePeriod;
type BalanceOf<T> = <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::Balance;
92

93
#[allow(dead_code)]
94
95
96
97
98
99
100
101
102
type NegativeImbalanceOf<T> = <CurrencyOf<T> as Currency<<T as frame_system::Config>::AccountId>>::NegativeImbalance;

type TrieIndex = u32;

pub trait WeightInfo {
	fn create() -> Weight;
	fn contribute() -> Weight;
	fn withdraw() -> Weight;
	fn dissolve(k: u32, ) -> Weight;
103
	fn edit() -> Weight;
104
105
	fn on_initialize(n: u32, ) -> Weight;
}
106

107
108
109
110
111
112
pub struct TestWeightInfo;
impl WeightInfo for TestWeightInfo {
	fn create() -> Weight { 0 }
	fn contribute() -> Weight { 0 }
	fn withdraw() -> Weight { 0 }
	fn dissolve(_k: u32, ) -> Weight { 0 }
113
	fn edit() -> Weight { 0 }
114
115
116
117
	fn on_initialize(_n: u32, ) -> Weight { 0 }
}

pub trait Config: frame_system::Config {
118
	type Event: From<Event<Self>> + Into<<Self as frame_system::Config>::Event>;
119

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

123
	/// The amount to be held on deposit by the depositor of a crowdloan.
124
125
	type SubmissionDeposit: Get<BalanceOf<Self>>;

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

Shawn Tabrizi's avatar
Shawn Tabrizi committed
130
	/// The period of time (in blocks) after an unsuccessful crowdloan ending when
131
132
133
134
135
	/// 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
136
137
138

	/// Max number of storage keys to remove per extrinsic call.
	type RemoveKeysLimit: Get<u32>;
139

140
141
142
143
144
145
146
147
148
149
	/// The parachain registrar type. We jus use this to ensure that only the manager of a para is able to
	/// start a crowdloan for its slot.
	type Registrar: Registrar<AccountId=Self::AccountId>;

	/// The type representing the auctioning system.
	type Auctioneer: Auctioneer<
		AccountId=Self::AccountId,
		BlockNumber=Self::BlockNumber,
		LeasePeriod=Self::BlockNumber,
	>;
150

151
152
153
154
155
	/// Weight Information for the Extrinsics in the Pallet
	type WeightInfo: WeightInfo;
}

#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug)]
156
157
pub enum LastContribution<BlockNumber> {
	Never,
158
	PreEnding(u32),
159
160
161
	Ending(BlockNumber),
}

162
163
164
/// Information on a funding effort for a pre-existing parachain. We assume that the parachain ID
/// is known as it's used for the key of the storage item for which this is the value (`Funds`).
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug)]
165
#[codec(dumb_trait_bound)]
166
167
168
169
pub struct FundInfo<AccountId, Balance, BlockNumber, LeasePeriod> {
	/// True if the fund is being retired. This can only be set once and only when the current
	/// lease period is greater than the `last_slot`.
	retiring: bool,
170
	/// The owning account who placed the deposit.
171
	depositor: AccountId,
172
173
	/// An optional verifier. If exists, contributions must be signed by verifier.
	verifier: Option<MultiSigner>,
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
	/// 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.
192
	first_slot: LeasePeriod,
193
194
	/// Last slot in range to bid on; it's actually a LeasePeriod, but that's the same type as
	/// BlockNumber.
195
196
197
	last_slot: LeasePeriod,
	/// Index used for the child trie of this fund
	trie_index: TrieIndex,
198
199
200
}

decl_storage! {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
201
	trait Store for Module<T: Config> as Crowdloan {
202
		/// Info on all of the funds.
203
		Funds get(fn funds):
204
205
			map hasher(twox_64_concat) ParaId
			=> Option<FundInfo<T::AccountId, BalanceOf<T>, T::BlockNumber, LeasePeriodOf<T>>>;
206
207
208

		/// 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.
209
		NewRaise get(fn new_raise): Vec<ParaId>;
210
211

		/// The number of auctions that have entered into their ending period so far.
212
213
214
215
		EndingsCount get(fn endings_count): u32;

		/// Tracker for the next available trie index
		NextTrieIndex get(fn next_trie_index): u32;
216
217
218
219
220
	}
}

decl_event! {
	pub enum Event<T> where
221
		<T as frame_system::Config>::AccountId,
222
223
		Balance = BalanceOf<T>,
	{
Shawn Tabrizi's avatar
Shawn Tabrizi committed
224
		/// Create a new crowdloaning campaign. [fund_index]
225
		Created(ParaId),
226
		/// Contributed to a crowd sale. [who, fund_index, amount]
227
		Contributed(AccountId, ParaId, Balance),
228
		/// Withdrew full balance of a contributor. [who, fund_index, amount]
229
		Withdrew(AccountId, ParaId, Balance),
230
		/// Fund is placed into retirement. [fund_index]
231
		Retiring(ParaId),
Shawn Tabrizi's avatar
Shawn Tabrizi committed
232
233
		/// Fund is partially dissolved, i.e. there are some left over child
		/// keys that still need to be killed. [fund_index]
234
		PartiallyDissolved(ParaId),
235
		/// Fund is dissolved. [fund_index]
236
237
238
239
240
		Dissolved(ParaId),
		/// The deploy data of the funded parachain is set. [fund_index]
		DeployDataFixed(ParaId),
		/// On-boarding process for a winning parachain fund is completed. [find_index, parachain_id]
		Onboarded(ParaId, ParaId),
Shawn Tabrizi's avatar
Shawn Tabrizi committed
241
		/// The result of trying to submit a new bid to the Slots pallet.
242
		HandleBidResult(ParaId, DispatchResult),
243
244
		/// The configuration to a crowdloan has been edited. [fund_index]
		Edited(ParaId),
245
246
247
	}
}

248
decl_error! {
249
	pub enum Error for Module<T: Config> {
250
251
		/// The first slot needs to at least be less than 3 `max_value`.
		FirstSlotTooFarInFuture,
252
253
254
255
256
257
258
259
260
261
262
		/// 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.
263
		InvalidParaId,
264
265
266
267
268
269
		/// Contributions exceed maximum amount.
		CapExceeded,
		/// The contribution period has already ended.
		ContributionPeriodOver,
		/// The origin of this call is invalid.
		InvalidOrigin,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
270
		/// This crowdloan does not correspond to a parachain.
271
		NotParachain,
272
273
274
275
		/// This parachain lease is still active and retirement cannot yet begin.
		LeaseActive,
		/// This parachain's bid or lease is still active and withdraw cannot yet begin.
		BidOrLeaseActive,
276
277
278
279
		/// Funds have not yet been returned.
		FundsNotReturned,
		/// Fund has not yet retired.
		FundNotRetired,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
280
		/// The crowdloan has not yet ended.
281
		FundNotEnded,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
282
		/// There are no contributions stored in this crowdloan.
283
		NoContributions,
Shawn Tabrizi's avatar
Shawn Tabrizi committed
284
		/// This crowdloan has an active parachain and cannot be dissolved.
285
		HasActiveParachain,
286
287
		/// The crowdloan is not ready to dissolve. Potentially still has a slot or in retirement period.
		NotReadyToDissolve,
288
289
		/// Invalid signature.
		InvalidSignature,
290
291
292
	}
}

293
decl_module! {
294
	pub struct Module<T: Config> for enum Call where origin: <T as frame_system::Config>::Origin {
295
296
		type Error = Error<T>;

297
		const ModuleId: ModuleId = T::ModuleId::get();
298
299
300
		const MinContribution: BalanceOf<T> = T::MinContribution::get();
		const RemoveKeysLimit: u32 = T::RemoveKeysLimit::get();
		const RetirementPeriod: T::BlockNumber = T::RetirementPeriod::get();
301

302
		fn deposit_event() = default;
303

Shawn Tabrizi's avatar
Shawn Tabrizi committed
304
		/// Create a new crowdloaning campaign for a parachain slot deposit for the current auction.
305
306
307
		#[weight = T::WeightInfo::create()]
		pub fn create(origin,
			#[compact] index: ParaId,
308
			#[compact] cap: BalanceOf<T>,
309
310
			#[compact] first_slot: LeasePeriodOf<T>,
			#[compact] last_slot: LeasePeriodOf<T>,
311
312
			#[compact] end: T::BlockNumber,
			verifier: Option<MultiSigner>,
313
		) {
314
			let depositor = ensure_signed(origin)?;
315

316
317
318
			ensure!(first_slot <= last_slot, Error::<T>::LastSlotBeforeFirstSlot);
			let last_slot_limit = first_slot.checked_add(&3u32.into()).ok_or(Error::<T>::FirstSlotTooFarInFuture)?;
			ensure!(last_slot <= last_slot_limit, Error::<T>::LastSlotTooFarInFuture);
319
			ensure!(end > <frame_system::Pallet<T>>::block_number(), Error::<T>::CannotEndInPast);
320

321
322
323
324
325
326
327
328
			// There should not be an existing fund.
			ensure!(!Funds::<T>::contains_key(index), Error::<T>::FundNotEnded);

			let manager = T::Registrar::manager_of(index).ok_or(Error::<T>::InvalidParaId)?;
			ensure!(depositor == manager, Error::<T>::InvalidOrigin);

			let trie_index = Self::next_trie_index();
			let new_trie_index = trie_index.checked_add(1).ok_or(Error::<T>::Overflow)?;
329

Shawn Tabrizi's avatar
Shawn Tabrizi committed
330
			let deposit = T::SubmissionDeposit::get();
331

332
333
334
335
336
			CurrencyOf::<T>::reserve(&depositor, deposit)?;

			Funds::<T>::insert(index, FundInfo {
				retiring: false,
				depositor,
337
				verifier,
338
339
340
341
342
343
344
				deposit,
				raised: Zero::zero(),
				end,
				cap,
				last_contribution: LastContribution::Never,
				first_slot,
				last_slot,
345
				trie_index,
346
347
			});

348
349
			NextTrieIndex::put(new_trie_index);

350
351
			Self::deposit_event(RawEvent::Created(index));
		}
352

353
354
		/// 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
355
		/// slot is unable to be purchased and the timeout expires.
356
357
358
		#[weight = T::WeightInfo::contribute()]
		pub fn contribute(origin,
			#[compact] index: ParaId,
359
360
361
			#[compact] value: BalanceOf<T>,
			signature: Option<MultiSignature>
		) {
362
363
			let who = ensure_signed(origin)?;

364
			ensure!(value >= T::MinContribution::get(), Error::<T>::ContributionTooSmall);
365
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
366
367
			fund.raised  = fund.raised.checked_add(&value).ok_or(Error::<T>::Overflow)?;
			ensure!(fund.raised <= fund.cap, Error::<T>::CapExceeded);
368

Shawn Tabrizi's avatar
Shawn Tabrizi committed
369
			// Make sure crowdloan has not ended
370
			let now = <frame_system::Pallet<T>>::block_number();
371
			ensure!(now < fund.end, Error::<T>::ContributionPeriodOver);
372

373
			let old_balance = Self::contribution_get(fund.trie_index, &who);
374
375
376
377
378
379
380
381

			if let Some(ref verifier) = fund.verifier {
				let signature = signature.ok_or(Error::<T>::InvalidSignature)?;
				let payload = (index, &who, old_balance, value);
				let valid = payload.using_encoded(|encoded| signature.verify(encoded, &verifier.clone().into_account()));
				ensure!(valid, Error::<T>::InvalidSignature);
			}

382
			CurrencyOf::<T>::transfer(&who, &Self::fund_account_id(index), value, AllowDeath)?;
383

384
			let balance = old_balance.saturating_add(value);
385
			Self::contribution_put(fund.trie_index, &who, &balance);
386

387
			if T::Auctioneer::is_ending(now).is_some() {
388
389
390
391
392
393
				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
394
						NewRaise::append(index);
395
396
397
398
399
400
401
402
403
404
405
406
407
408
						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
409
						NewRaise::append(index);
410
411
412
413
414
						fund.last_contribution = LastContribution::PreEnding(endings_count);
					}
				}
			}

415
			Funds::<T>::insert(index, &fund);
416
417
418
419

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

420
		/// Withdraw full balance of a contributor.
421
		///
422
		/// Origin must be signed.
423
		///
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
		/// The fund must be either in, or ready for, retirement. For a fund to be *in* retirement, then the retirement
		/// flag must be set. For a fund to be ready for retirement, then:
		/// - it must not already be in retirement;
		/// - the amount of raised funds must be bigger than the _free_ balance of the account;
		/// - and either:
		///   - the block number must be at least `end`; or
		///   - the current lease period must be greater than the fund's `last_slot`.
		///
		/// In this case, the fund's retirement flag is set and its `end` is reset to the current block
		/// number.
		///
		/// - `who`: The account whose contribution should be withdrawn.
		/// - `index`: The parachain to whose crowdloan the contribution was made.
		#[weight = T::WeightInfo::withdraw()]
		pub fn withdraw(origin, who: T::AccountId, #[compact] index: ParaId) {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
439
			ensure_signed(origin)?;
440

441
			let mut fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
442
443

			// `fund.end` can represent the end of a failed crowdsale or the beginning of retirement
444
			let now = frame_system::Pallet::<T>::block_number();
445
446
447
448
449
450
			let current_lease_period = T::Auctioneer::lease_period_index();
			ensure!(now >= fund.end || current_lease_period > fund.last_slot, Error::<T>::FundNotEnded);

			let fund_account = Self::fund_account_id(index);
			// free balance must equal amount raised, otherwise a bid or lease must be active.
			ensure!(CurrencyOf::<T>::free_balance(&fund_account) == fund.raised, Error::<T>::BidOrLeaseActive);
451

452
			let balance = Self::contribution_get(fund.trie_index, &who);
453
			ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
454
455

			// Avoid using transfer to ensure we don't pay any fees.
456
			CurrencyOf::<T>::transfer(&fund_account, &who, balance, AllowDeath)?;
457

458
			Self::contribution_kill(fund.trie_index, &who);
459
			fund.raised = fund.raised.saturating_sub(balance);
460
461
462
463
			if !fund.retiring {
				fund.retiring = true;
				fund.end = now;
			}
464

465
			Funds::<T>::insert(index, &fund);
466
467
468

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

470
471
472
473
474
		/// Remove a fund after the retirement period has ended.
		///
		/// This places any deposits that were not withdrawn into the treasury.
		#[weight = T::WeightInfo::dissolve(T::RemoveKeysLimit::get())]
		pub fn dissolve(origin, #[compact] index: ParaId) -> DispatchResultWithPostInfo {
475
			let who = ensure_signed(origin)?;
476

477
			let fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
478
			let now = frame_system::Pallet::<T>::block_number();
479
			let dissolution = fund.end.saturating_add(T::RetirementPeriod::get());
480
481
482
483

			let can_dissolve = (fund.retiring && now >= dissolution) ||
				(fund.raised.is_zero() && who == fund.depositor);
			ensure!(can_dissolve, Error::<T>::NotReadyToDissolve);
484

Shawn Tabrizi's avatar
Shawn Tabrizi committed
485
			// Try killing the crowdloan child trie
486
487
488
			match Self::crowdloan_kill(fund.trie_index) {
				child::KillChildStorageResult::AllRemoved(num_removed) => {
					CurrencyOf::<T>::unreserve(&fund.depositor, fund.deposit);
489

Shawn Tabrizi's avatar
Shawn Tabrizi committed
490
					// Remove all other balance from the account into orphaned funds.
491
492
					let account = Self::fund_account_id(index);
					let (imbalance, _) = CurrencyOf::<T>::slash(&account, BalanceOf::<T>::max_value());
Shawn Tabrizi's avatar
Shawn Tabrizi committed
493
					T::OrphanedFunds::on_unbalanced(imbalance);
494

495
					Funds::<T>::remove(index);
496

Shawn Tabrizi's avatar
Shawn Tabrizi committed
497
					Self::deposit_event(RawEvent::Dissolved(index));
498
499

					Ok(Some(T::WeightInfo::dissolve(num_removed)).into())
Shawn Tabrizi's avatar
Shawn Tabrizi committed
500
				},
501
				child::KillChildStorageResult::SomeRemaining(num_removed) => {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
502
					Self::deposit_event(RawEvent::PartiallyDissolved(index));
503
					Ok(Some(T::WeightInfo::dissolve(num_removed)).into())
Shawn Tabrizi's avatar
Shawn Tabrizi committed
504
505
				}
			}
506
		}
507

508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
		/// Edit the configuration for an in-progress crowdloan.
		///
		/// Can only be called by Root origin.
		#[weight = T::WeightInfo::edit()]
		pub fn edit(origin,
			#[compact] index: ParaId,
			#[compact] cap: BalanceOf<T>,
			#[compact] first_slot: LeasePeriodOf<T>,
			#[compact] last_slot: LeasePeriodOf<T>,
			#[compact] end: T::BlockNumber,
			verifier: Option<MultiSigner>,
		) {
			ensure_root(origin)?;

			let fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;

			Funds::<T>::insert(index, FundInfo {
				retiring: fund.retiring,
				depositor: fund.depositor,
				verifier,
				deposit: fund.deposit,
				raised: fund.raised,
				end,
				cap,
				last_contribution: fund.last_contribution,
				first_slot,
				last_slot,
				trie_index: fund.trie_index,
			});

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

Shawn Tabrizi's avatar
Shawn Tabrizi committed
541
		fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight {
542
			if let Some(n) = T::Auctioneer::is_ending(n) {
543
544
545
546
				if n.is_zero() {
					// first block of ending period.
					EndingsCount::mutate(|c| *c += 1);
				}
547
548
549
				let new_raise = NewRaise::take();
				let new_raise_len = new_raise.len() as u32;
				for (fund, para_id) in new_raise.into_iter().filter_map(|i| Self::funds(i).map(|f| (f, i))) {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
550
551
					// 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`.
552
553
554
					let result = T::Auctioneer::place_bid(
						Self::fund_account_id(para_id),
						para_id,
555
556
557
558
						fund.first_slot,
						fund.last_slot,
						fund.raised,
					);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
559

560
					Self::deposit_event(RawEvent::HandleBidResult(para_id, result));
561
				}
562
563
564
				T::WeightInfo::on_initialize(new_raise_len)
			} else {
				T::DbWeight::get().reads(1)
565
566
567
568
569
			}
		}
	}
}

570
impl<T: Config> Module<T> {
571
572
573
574
	/// 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.
575
	pub fn fund_account_id(index: ParaId) -> T::AccountId {
576
		T::ModuleId::get().into_sub_account(index)
577
578
	}

579
	pub fn id_from_index(index: TrieIndex) -> child::ChildInfo {
580
		let mut buf = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
581
		buf.extend_from_slice(b"crowdloan");
582
		buf.extend_from_slice(&index.encode()[..]);
583
		child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref())
584
585
	}

586
	pub fn contribution_put(index: TrieIndex, who: &T::AccountId, balance: &BalanceOf<T>) {
587
		who.using_encoded(|b| child::put(&Self::id_from_index(index), b, balance));
588
589
	}

590
	pub fn contribution_get(index: TrieIndex, who: &T::AccountId) -> BalanceOf<T> {
591
		who.using_encoded(|b| child::get_or_default::<BalanceOf<T>>(
592
			&Self::id_from_index(index),
593
594
			b,
		))
595
596
	}

597
	pub fn contribution_kill(index: TrieIndex, who: &T::AccountId) {
598
		who.using_encoded(|b| child::kill(&Self::id_from_index(index), b));
599
600
	}

601
	pub fn crowdloan_kill(index: TrieIndex) -> child::KillChildStorageResult {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
602
		child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get()))
603
604
605
	}
}

606
607
608
609
610
611
612
613
614
615
impl<T: Config> crate::traits::OnSwap for Module<T> {
	fn on_swap(one: ParaId, other: ParaId) {
		Funds::<T>::mutate(one, |x|
			Funds::<T>::mutate(other, |y|
				sp_std::mem::swap(x, y)
			)
		)
	}
}

616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
#[cfg(any(feature = "runtime-benchmarks", test))]
mod crypto {
	use sp_core::ed25519;
	use sp_io::crypto::{ed25519_sign, ed25519_generate};
	use sp_std::{
		vec::Vec,
		convert::TryFrom,
	};
	use sp_runtime::{MultiSigner, MultiSignature};

	pub fn create_ed25519_pubkey(seed: Vec<u8>) -> MultiSigner {
		ed25519_generate(0.into(), Some(seed)).into()
	}

	pub fn create_ed25519_signature(payload: &[u8], pubkey: MultiSigner) -> MultiSignature {
		let edpubkey = ed25519::Public::try_from(pubkey).unwrap();
		let edsig = ed25519_sign(0.into(), &edpubkey, payload).unwrap();
		edsig.into()
	}
}

637
638
639
640
#[cfg(test)]
mod tests {
	use super::*;

641
	use std::{cell::RefCell, sync::Arc};
642
	use frame_support::{
643
		assert_ok, assert_noop, parameter_types,
644
645
		traits::{OnInitialize, OnFinalize},
	};
646
	use sp_core::H256;
647
	use primitives::v1::Id as ParaId;
648
649
	// 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.
650
	use sp_runtime::{
651
652
653
654
655
656
		testing::Header, traits::{BlakeTwo256, IdentityLookup},
	};
	use crate::{
		mock::TestRegistrar,
		traits::OnSwap,
		crowdloan,
657
	};
658
	use sp_keystore::{KeystoreExt, testing::KeyStore};
659
660
661
662
663
664
665
666
667
668

	type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic<Test>;
	type Block = frame_system::mocking::MockBlock<Test>;

	frame_support::construct_runtime!(
		pub enum Test where
			Block = Block,
			NodeBlock = Block,
			UncheckedExtrinsic = UncheckedExtrinsic,
		{
669
670
671
			System: frame_system::{Pallet, Call, Config, Storage, Event<T>},
			Balances: pallet_balances::{Pallet, Call, Storage, Config<T>, Event<T>},
			Crowdloan: crowdloan::{Pallet, Call, Storage, Event<T>},
Shawn Tabrizi's avatar
Shawn Tabrizi committed
672
		}
673
	);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
674

675
676
677
	parameter_types! {
		pub const BlockHashCount: u32 = 250;
	}
678

679
	impl frame_system::Config for Test {
680
		type BaseCallFilter = ();
681
682
683
		type BlockWeights = ();
		type BlockLength = ();
		type DbWeight = ();
684
		type Origin = Origin;
685
		type Call = Call;
686
687
688
689
690
691
692
		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
693
		type Event = Event;
694
695
		type BlockHashCount = BlockHashCount;
		type Version = ();
696
		type PalletInfo = PalletInfo;
697
		type AccountData = pallet_balances::AccountData<u64>;
Gavin Wood's avatar
Gavin Wood committed
698
		type OnNewAccount = ();
699
		type OnKilledAccount = ();
700
		type SystemWeightInfo = ();
701
		type SS58Prefix = ();
702
	}
703

704
	parameter_types! {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
705
		pub const ExistentialDeposit: u64 = 1;
706
	}
707

708
	impl pallet_balances::Config for Test {
709
		type Balance = u64;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
710
		type Event = Event;
711
		type DustRemoval = ();
712
		type ExistentialDeposit = ExistentialDeposit;
713
		type AccountStore = System;
714
		type MaxLocks = ();
715
		type WeightInfo = ();
716
717
	}

718
719
720
721
722
723
724
725
	#[derive(Copy, Clone, Eq, PartialEq, Debug)]
	struct BidPlaced {
		height: u64,
		bidder: u64,
		para: ParaId,
		first_slot: u64,
		last_slot: u64,
		amount: u64
726
727
	}
	thread_local! {
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
		static AUCTION: RefCell<Option<(u64, u64)>> = RefCell::new(None);
		static ENDING_PERIOD: RefCell<u64> = RefCell::new(5);
		static BIDS_PLACED: RefCell<Vec<BidPlaced>> = RefCell::new(Vec::new());
	}
	#[allow(unused)]
	fn set_ending_period(ending_period: u64) {
		ENDING_PERIOD.with(|p| *p.borrow_mut() = ending_period);
	}
	fn auction() -> Option<(u64, u64)> {
		AUCTION.with(|p| p.borrow().clone())
	}
	fn ending_period() -> u64 {
		ENDING_PERIOD.with(|p| p.borrow().clone())
	}
	fn bids() -> Vec<BidPlaced> {
		BIDS_PLACED.with(|p| p.borrow().clone())
744
745
	}

746
747
748
749
750
751
	pub struct TestAuctioneer;
	impl Auctioneer for TestAuctioneer {
		type AccountId = u64;
		type BlockNumber = u64;
		type LeasePeriod = u64;
		type Currency = Balances;
752

753
754
		fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult {
			assert!(lease_period_index >= Self::lease_period_index());
755

756
757
758
			let ending = System::block_number().saturating_add(duration);
			AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending)));
			Ok(())
759
760
		}

761
762
763
764
765
766
767
768
769
		fn is_ending(now: u64) -> Option<u64> {
			if let Some((_, early_end)) = auction() {
				if let Some(after_early_end) = now.checked_sub(early_end) {
					if after_early_end < ending_period() {
						return Some(after_early_end)
					}
				}
			}
			None
770
771
		}

772
773
774
775
776
777
		fn place_bid(
			bidder: u64,
			para: ParaId,
			first_slot: u64,
			last_slot: u64,
			amount: u64
778
		) -> DispatchResult {
779
780
781
			let height = System::block_number();
			BIDS_PLACED.with(|p| p.borrow_mut().push(BidPlaced { height, bidder, para, first_slot, last_slot, amount }));
			Ok(())
782
		}
783

784
785
		fn lease_period_index() -> u64 {
			System::block_number() / 20
786
787
788
789
790
791
792
		}
	}

	parameter_types! {
		pub const SubmissionDeposit: u64 = 1;
		pub const MinContribution: u64 = 10;
		pub const RetirementPeriod: u64 = 5;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
793
794
		pub const CrowdloanModuleId: ModuleId = ModuleId(*b"py/cfund");
		pub const RemoveKeysLimit: u32 = 10;
795
	}
796

797
	impl Config for Test {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
798
		type Event = Event;
799
800
801
		type SubmissionDeposit = SubmissionDeposit;
		type MinContribution = MinContribution;
		type RetirementPeriod = RetirementPeriod;
802
		type OrphanedFunds = ();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
803
804
		type ModuleId = CrowdloanModuleId;
		type RemoveKeysLimit = RemoveKeysLimit;
805
806
807
		type Registrar = TestRegistrar<Test>;
		type Auctioneer = TestAuctioneer;
		type WeightInfo = crate::crowdloan::TestWeightInfo;
808
809
	}

810
	use pallet_balances::Error as BalancesError;
811
812
813

	// This function basically just builds a genesis storage key/value store according to
	// our desired mockup.
Shawn Tabrizi's avatar
Shawn Tabrizi committed
814
	pub fn new_test_ext() -> sp_io::TestExternalities {
815
816
		let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
		pallet_balances::GenesisConfig::<Test>{
817
818
			balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
		}.assimilate_storage(&mut t).unwrap();
819
820
821
822
		let keystore = KeyStore::new();
		let mut t: sp_io::TestExternalities = t.into();
		t.register_extension(KeystoreExt(Arc::new(keystore)));
		t
823
824
	}

825
826
827
828
829
830
831
832
833
834
	fn new_para() -> ParaId {
		for i in 0.. {
			let para: ParaId = i.into();
			if TestRegistrar::<Test>::is_registered(para) { continue }
			assert_ok!(TestRegistrar::<Test>::register(1, para, Default::default(), Default::default()));
			return para;
		}
		unreachable!()
	}

835
836
	fn run_to_block(n: u64) {
		while System::block_number() < n {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
837
			Crowdloan::on_finalize(System::block_number());
838
839
840
841
842
			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());
Shawn Tabrizi's avatar
Shawn Tabrizi committed
843
			Crowdloan::on_initialize(System::block_number());
844
845
846
847
848
		}
	}

	#[test]
	fn basic_setup_works() {
849
		new_test_ext().execute_with(|| {
850
			assert_eq!(System::block_number(), 0);
851
852
			assert_eq!(Crowdloan::funds(ParaId::from(0)), None);
			let empty: Vec<ParaId> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
853
			assert_eq!(Crowdloan::new_raise(), empty);
854
			assert_eq!(Crowdloan::contribution_get(0u32, &1), 0);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
855
			assert_eq!(Crowdloan::endings_count(), 0);
856
857
858
859
860
861
862
863
864
865
866

			assert_ok!(TestAuctioneer::new_auction(5, 0));

			assert_eq!(bids(), vec![]);
			assert_ok!(TestAuctioneer::place_bid(1, 2.into(), 0, 3, 6));
			let b = BidPlaced { height: 0, bidder: 1, para: 2.into(), first_slot: 0, last_slot: 3, amount: 6 };
			assert_eq!(bids(), vec![b]);
			assert_eq!(TestAuctioneer::is_ending(4), None);
			assert_eq!(TestAuctioneer::is_ending(5), Some(0));
			assert_eq!(TestAuctioneer::is_ending(9), Some(4));
			assert_eq!(TestAuctioneer::is_ending(11), None);
867
868
869
870
871
		});
	}

	#[test]
	fn create_works() {
872
		new_test_ext().execute_with(|| {
873
			let para = new_para();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
874
			// Now try to create a crowdloan campaign
875
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
876
877
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
878
879
				retiring: false,
				depositor: 1,
880
881
882
883
884
885
886
887
888
				verifier: None,
				deposit: 1,
				raised: 0,
				// 5 blocks length + 3 block ending period + 1 starting block
				end: 9,
				cap: 1000,
				last_contribution: LastContribution::Never,
				first_slot: 1,
				last_slot: 4,
889
				trie_index: 0,
890
			};
891
			assert_eq!(Crowdloan::funds(para), Some(fund_info));
892
893
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
894
895
			// Deposit is placed in reserved
			assert_eq!(Balances::reserved_balance(1), 1);
896
			// No new raise until first contribution
897
			let empty: Vec<ParaId> = Vec::new();
898
899
900
901
902
903
904
905
			assert_eq!(Crowdloan::new_raise(), empty);
		});
	}

	#[test]
	fn create_with_verifier_works() {
		new_test_ext().execute_with(|| {
			let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
906
			let para = new_para();
907
			// Now try to create a crowdloan campaign
908
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, Some(pubkey.clone())));
909
910
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
911
912
				retiring: false,
				depositor: 1,
913
				verifier: Some(pubkey),
914
915
916
917
918
919
920
921
				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,
922
				trie_index: 0,
923
			};
924
			assert_eq!(Crowdloan::funds(ParaId::from(0)), Some(fund_info));
925
926
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
927
928
			// Deposit is placed in reserved
			assert_eq!(Balances::reserved_balance(1), 1);
929
			// No new raise until first contribution
930
			let empty: Vec<ParaId> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
931
			assert_eq!(Crowdloan::new_raise(), empty);
932
933
934
935
936
		});
	}

	#[test]
	fn create_handles_basic_errors() {
937
		new_test_ext().execute_with(|| {
938
939
940
941
942
			// Now try to create a crowdloan campaign
			let para = new_para();

			let e = Error::<Test>::InvalidParaId;
			assert_noop!(Crowdloan::create(Origin::signed(1), 1.into(), 1000, 1, 4, 9, None), e);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
943
			// Cannot create a crowdloan with bad slots
944
945
946
947
			let e = Error::<Test>::LastSlotBeforeFirstSlot;
			assert_noop!(Crowdloan::create(Origin::signed(1), para, 1000, 4, 1, 9, None), e);
			let e = Error::<Test>::LastSlotTooFarInFuture;
			assert_noop!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 5, 9, None), e);
948

Shawn Tabrizi's avatar
Shawn Tabrizi committed
949
			// Cannot create a crowdloan without some deposit funds
950
951
952
			assert_ok!(TestRegistrar::<Test>::register(1337, ParaId::from(1234), Default::default(), Default::default()));
			let e = BalancesError::<Test, _>::InsufficientBalance;
			assert_noop!(Crowdloan::create(Origin::signed(1337), ParaId::from(1234), 1000, 1, 3, 9, None), e);
953
954
955
956
957
		});
	}

	#[test]
	fn contribute_works() {
958
		new_test_ext().execute_with(|| {
959
960
			let para = new_para();

Shawn Tabrizi's avatar
Shawn Tabrizi committed
961
			// Set up a crowdloan
962
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
963
964

			// No contributions yet
965
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 0);
966

Shawn Tabrizi's avatar
Shawn Tabrizi committed
967
			// User 1 contributes to their own crowdloan
968
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 49, None));
969
			// User 1 has spent some funds to do this, transfer fees **are** taken
Gavin Wood's avatar
Gavin Wood committed
970
			assert_eq!(Balances::free_balance(1), 950);
971
			// Contributions are stored in the trie
972
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 49);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
973
			// Contributions appear in free balance of crowdloan
974
			assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(para)), 49);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
975
			// Crowdloan is added to NewRaise
976
			assert_eq!(Crowdloan::new_raise(), vec![para]);
977

978
			let fund = Crowdloan::funds(para).unwrap();
979
980
981
982
983
984
985

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

986
987
988
	#[test]
	fn contribute_with_verifier_works() {
		new_test_ext().execute_with(|| {
989
			let para = new_para();
990
991
			let pubkey = crypto::create_ed25519_pubkey(b"//verifier".to_vec());
			// Set up a crowdloan
992
993
994
995
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, Some(pubkey.clone())));

			// No contributions yet
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 0);
996
997

			// Missing signature
998
			assert_noop!(Crowdloan::contribute(Origin::signed(1), para, 49, None), Error::<Test>::InvalidSignature);
999
1000

			let payload = (0u32, 1u64, 0u64, 49u64);
For faster browsing, not all history is shown. View entire blame