crowdloan.rs 56.3 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 specific contributor.
421
		///
422
		/// Origin must be signed, but can come from anyone.
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
			let now = frame_system::Pallet::<T>::block_number();
443
			let fund_account = Self::fund_account_id(index);
444
			Self::ensure_crowdloan_ended(now, &fund_account, &fund)?;
445

446
			let balance = Self::contribution_get(fund.trie_index, &who);
447
			ensure!(balance > Zero::zero(), Error::<T>::NoContributions);
448

449
			CurrencyOf::<T>::transfer(&fund_account, &who, balance, AllowDeath)?;
450

451
			Self::contribution_kill(fund.trie_index, &who);
452
			fund.raised = fund.raised.saturating_sub(balance);
453
454
455
456
			if !fund.retiring {
				fund.retiring = true;
				fund.end = now;
			}
457

458
			Funds::<T>::insert(index, &fund);
459
460
461

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

463
		/// Remove a fund after the retirement period has ended and all funds have been returned.
464
465
466
467
		///
		/// 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 {
468
			let who = ensure_signed(origin)?;
469

470
			let fund = Self::funds(index).ok_or(Error::<T>::InvalidParaId)?;
471
			let now = frame_system::Pallet::<T>::block_number();
472
			let dissolution = fund.end.saturating_add(T::RetirementPeriod::get());
473
474
475
476

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

Shawn Tabrizi's avatar
Shawn Tabrizi committed
478
			// Try killing the crowdloan child trie
479
480
481
			match Self::crowdloan_kill(fund.trie_index) {
				child::KillChildStorageResult::AllRemoved(num_removed) => {
					CurrencyOf::<T>::unreserve(&fund.depositor, fund.deposit);
482

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

488
					Funds::<T>::remove(index);
489

Shawn Tabrizi's avatar
Shawn Tabrizi committed
490
					Self::deposit_event(RawEvent::Dissolved(index));
491
492

					Ok(Some(T::WeightInfo::dissolve(num_removed)).into())
Shawn Tabrizi's avatar
Shawn Tabrizi committed
493
				},
494
				child::KillChildStorageResult::SomeRemaining(num_removed) => {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
495
					Self::deposit_event(RawEvent::PartiallyDissolved(index));
496
					Ok(Some(T::WeightInfo::dissolve(num_removed)).into())
Shawn Tabrizi's avatar
Shawn Tabrizi committed
497
498
				}
			}
499
		}
500

501
502
503
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
		/// 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
534
		fn on_initialize(n: T::BlockNumber) -> frame_support::weights::Weight {
535
			if let Some(n) = T::Auctioneer::is_ending(n) {
536
537
538
539
				if n.is_zero() {
					// first block of ending period.
					EndingsCount::mutate(|c| *c += 1);
				}
540
541
542
				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
543
544
					// 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`.
545
546
547
					let result = T::Auctioneer::place_bid(
						Self::fund_account_id(para_id),
						para_id,
548
549
550
551
						fund.first_slot,
						fund.last_slot,
						fund.raised,
					);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
552

553
					Self::deposit_event(RawEvent::HandleBidResult(para_id, result));
554
				}
555
556
557
				T::WeightInfo::on_initialize(new_raise_len)
			} else {
				T::DbWeight::get().reads(1)
558
559
560
561
562
			}
		}
	}
}

563
impl<T: Config> Module<T> {
564
565
566
567
	/// 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.
568
	pub fn fund_account_id(index: ParaId) -> T::AccountId {
569
		T::ModuleId::get().into_sub_account(index)
570
571
	}

572
	pub fn id_from_index(index: TrieIndex) -> child::ChildInfo {
573
		let mut buf = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
574
		buf.extend_from_slice(b"crowdloan");
575
		buf.extend_from_slice(&index.encode()[..]);
576
		child::ChildInfo::new_default(T::Hashing::hash(&buf[..]).as_ref())
577
578
	}

579
	pub fn contribution_put(index: TrieIndex, who: &T::AccountId, balance: &BalanceOf<T>) {
580
		who.using_encoded(|b| child::put(&Self::id_from_index(index), b, balance));
581
582
	}

583
	pub fn contribution_get(index: TrieIndex, who: &T::AccountId) -> BalanceOf<T> {
584
		who.using_encoded(|b| child::get_or_default::<BalanceOf<T>>(
585
			&Self::id_from_index(index),
586
587
			b,
		))
588
589
	}

590
	pub fn contribution_kill(index: TrieIndex, who: &T::AccountId) {
591
		who.using_encoded(|b| child::kill(&Self::id_from_index(index), b));
592
593
	}

594
	pub fn crowdloan_kill(index: TrieIndex) -> child::KillChildStorageResult {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
595
		child::kill_storage(&Self::id_from_index(index), Some(T::RemoveKeysLimit::get()))
596
	}
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617

	/// This function checks all conditions which would qualify a crowdloan has ended.
	/// * If we have reached the `fund.end` block OR the first lease period the fund is
	///   trying to bid for has started already.
	/// * And, if the fund has enough free funds to refund full raised amount.
	fn ensure_crowdloan_ended(
		now: T::BlockNumber,
		fund_account: &T::AccountId,
		fund: &FundInfo<T::AccountId, BalanceOf<T>, T::BlockNumber, LeasePeriodOf<T>>
	) -> DispatchResult {
			// `fund.end` can represent the end of a failed crowdsale or the beginning of retirement
			// If the current lease period is past the first slot they are trying to bid for, then it is already too late
			// to win the bid.
			let current_lease_period = T::Auctioneer::lease_period_index();
			ensure!(now >= fund.end || current_lease_period > fund.first_slot, Error::<T>::FundNotEnded);
			// free balance must greater than or equal amount raised, otherwise funds are being used
			// and a bid or lease must be active.
			ensure!(CurrencyOf::<T>::free_balance(&fund_account) >= fund.raised, Error::<T>::BidOrLeaseActive);

			Ok(())
	}
618
619
}

620
621
622
623
624
625
626
627
628
629
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)
			)
		)
	}
}

630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
#[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()
	}
}

651
652
653
654
#[cfg(test)]
mod tests {
	use super::*;

655
	use std::{cell::RefCell, sync::Arc};
656
	use frame_support::{
657
		assert_ok, assert_noop, parameter_types,
658
659
		traits::{OnInitialize, OnFinalize},
	};
660
	use sp_core::H256;
661
	use primitives::v1::Id as ParaId;
662
663
	// 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.
664
	use sp_runtime::{
665
666
667
668
669
670
		testing::Header, traits::{BlakeTwo256, IdentityLookup},
	};
	use crate::{
		mock::TestRegistrar,
		traits::OnSwap,
		crowdloan,
671
	};
672
	use sp_keystore::{KeystoreExt, testing::KeyStore};
673
674
675
676
677
678
679
680
681
682

	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,
		{
683
684
685
			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
686
		}
687
	);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
688

689
690
691
	parameter_types! {
		pub const BlockHashCount: u32 = 250;
	}
692

693
	impl frame_system::Config for Test {
694
		type BaseCallFilter = ();
695
696
697
		type BlockWeights = ();
		type BlockLength = ();
		type DbWeight = ();
698
		type Origin = Origin;
699
		type Call = Call;
700
701
702
703
704
705
706
		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
707
		type Event = Event;
708
709
		type BlockHashCount = BlockHashCount;
		type Version = ();
710
		type PalletInfo = PalletInfo;
711
		type AccountData = pallet_balances::AccountData<u64>;
Gavin Wood's avatar
Gavin Wood committed
712
		type OnNewAccount = ();
713
		type OnKilledAccount = ();
714
		type SystemWeightInfo = ();
715
		type SS58Prefix = ();
716
	}
717

718
	parameter_types! {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
719
		pub const ExistentialDeposit: u64 = 1;
720
	}
721

722
	impl pallet_balances::Config for Test {
723
		type Balance = u64;
Shawn Tabrizi's avatar
Shawn Tabrizi committed
724
		type Event = Event;
725
		type DustRemoval = ();
726
		type ExistentialDeposit = ExistentialDeposit;
727
		type AccountStore = System;
728
		type MaxLocks = ();
729
		type WeightInfo = ();
730
731
	}

732
733
734
735
736
737
738
739
	#[derive(Copy, Clone, Eq, PartialEq, Debug)]
	struct BidPlaced {
		height: u64,
		bidder: u64,
		para: ParaId,
		first_slot: u64,
		last_slot: u64,
		amount: u64
740
741
	}
	thread_local! {
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
		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())
758
759
	}

760
761
762
763
764
765
	pub struct TestAuctioneer;
	impl Auctioneer for TestAuctioneer {
		type AccountId = u64;
		type BlockNumber = u64;
		type LeasePeriod = u64;
		type Currency = Balances;
766

767
768
		fn new_auction(duration: u64, lease_period_index: u64) -> DispatchResult {
			assert!(lease_period_index >= Self::lease_period_index());
769

770
771
772
			let ending = System::block_number().saturating_add(duration);
			AUCTION.with(|p| *p.borrow_mut() = Some((lease_period_index, ending)));
			Ok(())
773
774
		}

775
776
777
778
779
780
781
782
783
		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
784
785
		}

786
787
788
789
790
791
		fn place_bid(
			bidder: u64,
			para: ParaId,
			first_slot: u64,
			last_slot: u64,
			amount: u64
792
		) -> DispatchResult {
793
794
795
			let height = System::block_number();
			BIDS_PLACED.with(|p| p.borrow_mut().push(BidPlaced { height, bidder, para, first_slot, last_slot, amount }));
			Ok(())
796
		}
797

798
799
		fn lease_period_index() -> u64 {
			System::block_number() / 20
800
801
802
803
804
805
806
		}
	}

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

811
	impl Config for Test {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
812
		type Event = Event;
813
814
815
		type SubmissionDeposit = SubmissionDeposit;
		type MinContribution = MinContribution;
		type RetirementPeriod = RetirementPeriod;
816
		type OrphanedFunds = ();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
817
818
		type ModuleId = CrowdloanModuleId;
		type RemoveKeysLimit = RemoveKeysLimit;
819
820
821
		type Registrar = TestRegistrar<Test>;
		type Auctioneer = TestAuctioneer;
		type WeightInfo = crate::crowdloan::TestWeightInfo;
822
823
	}

824
	use pallet_balances::Error as BalancesError;
825
826
827

	// This function basically just builds a genesis storage key/value store according to
	// our desired mockup.
Shawn Tabrizi's avatar
Shawn Tabrizi committed
828
	pub fn new_test_ext() -> sp_io::TestExternalities {
829
830
		let mut t = frame_system::GenesisConfig::default().build_storage::<Test>().unwrap();
		pallet_balances::GenesisConfig::<Test>{
831
832
			balances: vec![(1, 1000), (2, 2000), (3, 3000), (4, 4000)],
		}.assimilate_storage(&mut t).unwrap();
833
834
835
836
		let keystore = KeyStore::new();
		let mut t: sp_io::TestExternalities = t.into();
		t.register_extension(KeystoreExt(Arc::new(keystore)));
		t
837
838
	}

839
840
841
842
843
844
845
846
847
848
	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!()
	}

849
850
	fn run_to_block(n: u64) {
		while System::block_number() < n {
Shawn Tabrizi's avatar
Shawn Tabrizi committed
851
			Crowdloan::on_finalize(System::block_number());
852
853
854
855
856
			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
857
			Crowdloan::on_initialize(System::block_number());
858
859
860
861
862
		}
	}

	#[test]
	fn basic_setup_works() {
863
		new_test_ext().execute_with(|| {
864
			assert_eq!(System::block_number(), 0);
865
866
			assert_eq!(Crowdloan::funds(ParaId::from(0)), None);
			let empty: Vec<ParaId> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
867
			assert_eq!(Crowdloan::new_raise(), empty);
868
			assert_eq!(Crowdloan::contribution_get(0u32, &1), 0);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
869
			assert_eq!(Crowdloan::endings_count(), 0);
870
871
872
873
874
875
876
877
878
879
880

			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);
881
882
883
884
885
		});
	}

	#[test]
	fn create_works() {
886
		new_test_ext().execute_with(|| {
887
			let para = new_para();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
888
			// Now try to create a crowdloan campaign
889
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
890
891
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
892
893
				retiring: false,
				depositor: 1,
894
895
896
897
898
899
900
901
902
				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,
903
				trie_index: 0,
904
			};
905
			assert_eq!(Crowdloan::funds(para), Some(fund_info));
906
907
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
908
909
			// Deposit is placed in reserved
			assert_eq!(Balances::reserved_balance(1), 1);
910
			// No new raise until first contribution
911
			let empty: Vec<ParaId> = Vec::new();
912
913
914
915
916
917
918
919
			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());
920
			let para = new_para();
921
			// Now try to create a crowdloan campaign
922
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, Some(pubkey.clone())));
923
924
			// This is what the initial `fund_info` should look like
			let fund_info = FundInfo {
925
926
				retiring: false,
				depositor: 1,
927
				verifier: Some(pubkey),
928
929
930
931
932
933
934
935
				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,
936
				trie_index: 0,
937
			};
938
			assert_eq!(Crowdloan::funds(ParaId::from(0)), Some(fund_info));
939
940
			// User has deposit removed from their free balance
			assert_eq!(Balances::free_balance(1), 999);
941
942
			// Deposit is placed in reserved
			assert_eq!(Balances::reserved_balance(1), 1);
943
			// No new raise until first contribution
944
			let empty: Vec<ParaId> = Vec::new();
Shawn Tabrizi's avatar
Shawn Tabrizi committed
945
			assert_eq!(Crowdloan::new_raise(), empty);
946
947
948
949
950
		});
	}

	#[test]
	fn create_handles_basic_errors() {
951
		new_test_ext().execute_with(|| {
952
953
954
955
956
			// 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
957
			// Cannot create a crowdloan with bad slots
958
959
960
961
			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);
962

Shawn Tabrizi's avatar
Shawn Tabrizi committed
963
			// Cannot create a crowdloan without some deposit funds
964
965
966
			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);
967
968
969
970
971
		});
	}

	#[test]
	fn contribute_works() {
972
		new_test_ext().execute_with(|| {
973
974
			let para = new_para();

Shawn Tabrizi's avatar
Shawn Tabrizi committed
975
			// Set up a crowdloan
976
			assert_ok!(Crowdloan::create(Origin::signed(1), para, 1000, 1, 4, 9, None));
977
978

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

Shawn Tabrizi's avatar
Shawn Tabrizi committed
981
			// User 1 contributes to their own crowdloan
982
			assert_ok!(Crowdloan::contribute(Origin::signed(1), para, 49, None));
983
			// User 1 has spent some funds to do this, transfer fees **are** taken
Gavin Wood's avatar
Gavin Wood committed
984
			assert_eq!(Balances::free_balance(1), 950);
985
			// Contributions are stored in the trie
986
			assert_eq!(Crowdloan::contribution_get(u32::from(para), &1), 49);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
987
			// Contributions appear in free balance of crowdloan
988
			assert_eq!(Balances::free_balance(Crowdloan::fund_account_id(para)), 49);
Shawn Tabrizi's avatar
Shawn Tabrizi committed
989
			// Crowdloan is added to NewRaise
990
			assert_eq!(Crowdloan::new_raise(), vec![para]);
991

992
			let fund = Crowdloan::funds(para).unwrap();
993
994
995
996
997
998
999

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

1000
	#[test]
For faster browsing, not all history is shown. View entire blame