referrerpolicy=no-referrer-when-downgrade

polkadot_runtime_common/purchase/
mod.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// This file is part of Polkadot.
3
4// Polkadot is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Polkadot is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Polkadot.  If not, see <http://www.gnu.org/licenses/>.
16
17//! Pallet to process purchase of DOTs.
18
19use alloc::vec::Vec;
20use codec::{Decode, Encode};
21use frame_support::{
22	pallet_prelude::*,
23	traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule},
24};
25use frame_system::pallet_prelude::*;
26pub use pallet::*;
27use scale_info::TypeInfo;
28use sp_core::sr25519;
29use sp_runtime::{
30	traits::{CheckedAdd, Saturating, Verify, Zero},
31	AnySignature, DispatchError, DispatchResult, Permill, RuntimeDebug,
32};
33
34type BalanceOf<T> =
35	<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
36
37/// The kind of statement an account needs to make for a claim to be valid.
38#[derive(
39	Encode, Decode, DecodeWithMemTracking, Clone, Copy, Eq, PartialEq, RuntimeDebug, TypeInfo,
40)]
41pub enum AccountValidity {
42	/// Account is not valid.
43	Invalid,
44	/// Account has initiated the account creation process.
45	Initiated,
46	/// Account is pending validation.
47	Pending,
48	/// Account is valid with a low contribution amount.
49	ValidLow,
50	/// Account is valid with a high contribution amount.
51	ValidHigh,
52	/// Account has completed the purchase process.
53	Completed,
54}
55
56impl Default for AccountValidity {
57	fn default() -> Self {
58		AccountValidity::Invalid
59	}
60}
61
62impl AccountValidity {
63	fn is_valid(&self) -> bool {
64		match self {
65			Self::Invalid => false,
66			Self::Initiated => false,
67			Self::Pending => false,
68			Self::ValidLow => true,
69			Self::ValidHigh => true,
70			Self::Completed => false,
71		}
72	}
73}
74
75/// All information about an account regarding the purchase of DOTs.
76#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)]
77pub struct AccountStatus<Balance> {
78	/// The current validity status of the user. Will denote if the user has passed KYC,
79	/// how much they are able to purchase, and when their purchase process has completed.
80	validity: AccountValidity,
81	/// The amount of free DOTs they have purchased.
82	free_balance: Balance,
83	/// The amount of locked DOTs they have purchased.
84	locked_balance: Balance,
85	/// Their sr25519/ed25519 signature verifying they have signed our required statement.
86	signature: Vec<u8>,
87	/// The percentage of VAT the purchaser is responsible for. This is already factored into
88	/// account balance.
89	vat: Permill,
90}
91
92#[frame_support::pallet]
93pub mod pallet {
94	use super::*;
95
96	#[pallet::pallet]
97	#[pallet::without_storage_info]
98	pub struct Pallet<T>(_);
99
100	#[pallet::config]
101	pub trait Config: frame_system::Config {
102		/// The overarching event type.
103		#[allow(deprecated)]
104		type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
105
106		/// Balances Pallet
107		type Currency: Currency<Self::AccountId>;
108
109		/// Vesting Pallet
110		type VestingSchedule: VestingSchedule<
111			Self::AccountId,
112			Moment = BlockNumberFor<Self>,
113			Currency = Self::Currency,
114		>;
115
116		/// The origin allowed to set account status.
117		type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>;
118
119		/// The origin allowed to make configurations to the pallet.
120		type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>;
121
122		/// The maximum statement length for the statement users to sign when creating an account.
123		#[pallet::constant]
124		type MaxStatementLength: Get<u32>;
125
126		/// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
127		#[pallet::constant]
128		type UnlockedProportion: Get<Permill>;
129
130		/// The maximum amount of locked DOTs that we will unlock.
131		#[pallet::constant]
132		type MaxUnlocked: Get<BalanceOf<Self>>;
133	}
134
135	#[pallet::event]
136	#[pallet::generate_deposit(pub(super) fn deposit_event)]
137	pub enum Event<T: Config> {
138		/// A new account was created.
139		AccountCreated { who: T::AccountId },
140		/// Someone's account validity was updated.
141		ValidityUpdated { who: T::AccountId, validity: AccountValidity },
142		/// Someone's purchase balance was updated.
143		BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
144		/// A payout was made to a purchaser.
145		PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> },
146		/// A new payment account was set.
147		PaymentAccountSet { who: T::AccountId },
148		/// A new statement was set.
149		StatementUpdated,
150		/// A new statement was set. `[block_number]`
151		UnlockBlockUpdated { block_number: BlockNumberFor<T> },
152	}
153
154	#[pallet::error]
155	pub enum Error<T> {
156		/// Account is not currently valid to use.
157		InvalidAccount,
158		/// Account used in the purchase already exists.
159		ExistingAccount,
160		/// Provided signature is invalid
161		InvalidSignature,
162		/// Account has already completed the purchase process.
163		AlreadyCompleted,
164		/// An overflow occurred when doing calculations.
165		Overflow,
166		/// The statement is too long to be stored on chain.
167		InvalidStatement,
168		/// The unlock block is in the past!
169		InvalidUnlockBlock,
170		/// Vesting schedule already exists for this account.
171		VestingScheduleExists,
172	}
173
174	// A map of all participants in the DOT purchase process.
175	#[pallet::storage]
176	pub(super) type Accounts<T: Config> =
177		StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>;
178
179	// The account that will be used to payout participants of the DOT purchase process.
180	#[pallet::storage]
181	pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>;
182
183	// The statement purchasers will need to sign to participate.
184	#[pallet::storage]
185	pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>;
186
187	// The block where all locked dots will unlock.
188	#[pallet::storage]
189	pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>;
190
191	#[pallet::hooks]
192	impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {}
193
194	#[pallet::call]
195	impl<T: Config> Pallet<T> {
196		/// Create a new account. Proof of existence through a valid signed message.
197		///
198		/// We check that the account does not exist at this stage.
199		///
200		/// Origin must match the `ValidityOrigin`.
201		#[pallet::call_index(0)]
202		#[pallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))]
203		pub fn create_account(
204			origin: OriginFor<T>,
205			who: T::AccountId,
206			signature: Vec<u8>,
207		) -> DispatchResult {
208			T::ValidityOrigin::ensure_origin(origin)?;
209			// Account is already being tracked by the pallet.
210			ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount);
211			// Account should not have a vesting schedule.
212			ensure!(
213				T::VestingSchedule::vesting_balance(&who).is_none(),
214				Error::<T>::VestingScheduleExists
215			);
216
217			// Verify the signature provided is valid for the statement.
218			Self::verify_signature(&who, &signature)?;
219
220			// Create a new pending account.
221			let status = AccountStatus {
222				validity: AccountValidity::Initiated,
223				signature,
224				free_balance: Zero::zero(),
225				locked_balance: Zero::zero(),
226				vat: Permill::zero(),
227			};
228			Accounts::<T>::insert(&who, status);
229			Self::deposit_event(Event::<T>::AccountCreated { who });
230			Ok(())
231		}
232
233		/// Update the validity status of an existing account. If set to completed, the account
234		/// will no longer be able to continue through the crowdfund process.
235		///
236		/// We check that the account exists at this stage, but has not completed the process.
237		///
238		/// Origin must match the `ValidityOrigin`.
239		#[pallet::call_index(1)]
240		#[pallet::weight(T::DbWeight::get().reads_writes(1, 1))]
241		pub fn update_validity_status(
242			origin: OriginFor<T>,
243			who: T::AccountId,
244			validity: AccountValidity,
245		) -> DispatchResult {
246			T::ValidityOrigin::ensure_origin(origin)?;
247			ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount);
248			Accounts::<T>::try_mutate(
249				&who,
250				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
251					ensure!(
252						status.validity != AccountValidity::Completed,
253						Error::<T>::AlreadyCompleted
254					);
255					status.validity = validity;
256					Ok(())
257				},
258			)?;
259			Self::deposit_event(Event::<T>::ValidityUpdated { who, validity });
260			Ok(())
261		}
262
263		/// Update the balance of a valid account.
264		///
265		/// We check that the account is valid for a balance transfer at this point.
266		///
267		/// Origin must match the `ValidityOrigin`.
268		#[pallet::call_index(2)]
269		#[pallet::weight(T::DbWeight::get().reads_writes(2, 1))]
270		pub fn update_balance(
271			origin: OriginFor<T>,
272			who: T::AccountId,
273			free_balance: BalanceOf<T>,
274			locked_balance: BalanceOf<T>,
275			vat: Permill,
276		) -> DispatchResult {
277			T::ValidityOrigin::ensure_origin(origin)?;
278
279			Accounts::<T>::try_mutate(
280				&who,
281				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
282					// Account has a valid status (not Invalid, Pending, or Completed)...
283					ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
284
285					free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?;
286					status.free_balance = free_balance;
287					status.locked_balance = locked_balance;
288					status.vat = vat;
289					Ok(())
290				},
291			)?;
292			Self::deposit_event(Event::<T>::BalanceUpdated {
293				who,
294				free: free_balance,
295				locked: locked_balance,
296			});
297			Ok(())
298		}
299
300		/// Pay the user and complete the purchase process.
301		///
302		/// We reverify all assumptions about the state of an account, and complete the process.
303		///
304		/// Origin must match the configured `PaymentAccount` (if it is not configured then this
305		/// will always fail with `BadOrigin`).
306		#[pallet::call_index(3)]
307		#[pallet::weight(T::DbWeight::get().reads_writes(4, 2))]
308		pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
309			// Payments must be made directly by the `PaymentAccount`.
310			let payment_account = ensure_signed(origin)?;
311			let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?;
312			ensure!(payment_account == test_against, DispatchError::BadOrigin);
313
314			// Account should not have a vesting schedule.
315			ensure!(
316				T::VestingSchedule::vesting_balance(&who).is_none(),
317				Error::<T>::VestingScheduleExists
318			);
319
320			Accounts::<T>::try_mutate(
321				&who,
322				|status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult {
323					// Account has a valid status (not Invalid, Pending, or Completed)...
324					ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount);
325
326					// Transfer funds from the payment account into the purchasing user.
327					let total_balance = status
328						.free_balance
329						.checked_add(&status.locked_balance)
330						.ok_or(Error::<T>::Overflow)?;
331					T::Currency::transfer(
332						&payment_account,
333						&who,
334						total_balance,
335						ExistenceRequirement::AllowDeath,
336					)?;
337
338					if !status.locked_balance.is_zero() {
339						let unlock_block = UnlockBlock::<T>::get();
340						// We allow some configurable portion of the purchased locked DOTs to be
341						// unlocked for basic usage.
342						let unlocked = (T::UnlockedProportion::get() * status.locked_balance)
343							.min(T::MaxUnlocked::get());
344						let locked = status.locked_balance.saturating_sub(unlocked);
345						// We checked that this account has no existing vesting schedule. So this
346						// function should never fail, however if it does, not much we can do about
347						// it at this point.
348						let _ = T::VestingSchedule::add_vesting_schedule(
349							// Apply vesting schedule to this user
350							&who,
351							// For this much amount
352							locked,
353							// Unlocking the full amount after one block
354							locked,
355							// When everything unlocks
356							unlock_block,
357						);
358					}
359
360					// Setting the user account to `Completed` ends the purchase process for this
361					// user.
362					status.validity = AccountValidity::Completed;
363					Self::deposit_event(Event::<T>::PaymentComplete {
364						who: who.clone(),
365						free: status.free_balance,
366						locked: status.locked_balance,
367					});
368					Ok(())
369				},
370			)?;
371			Ok(())
372		}
373
374		/* Configuration Operations */
375
376		/// Set the account that will be used to payout users in the DOT purchase process.
377		///
378		/// Origin must match the `ConfigurationOrigin`
379		#[pallet::call_index(4)]
380		#[pallet::weight(T::DbWeight::get().writes(1))]
381		pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult {
382			T::ConfigurationOrigin::ensure_origin(origin)?;
383			// Possibly this is worse than having the caller account be the payment account?
384			PaymentAccount::<T>::put(who.clone());
385			Self::deposit_event(Event::<T>::PaymentAccountSet { who });
386			Ok(())
387		}
388
389		/// Set the statement that must be signed for a user to participate on the DOT sale.
390		///
391		/// Origin must match the `ConfigurationOrigin`
392		#[pallet::call_index(5)]
393		#[pallet::weight(T::DbWeight::get().writes(1))]
394		pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult {
395			T::ConfigurationOrigin::ensure_origin(origin)?;
396			ensure!(
397				(statement.len() as u32) < T::MaxStatementLength::get(),
398				Error::<T>::InvalidStatement
399			);
400			// Possibly this is worse than having the caller account be the payment account?
401			Statement::<T>::set(statement);
402			Self::deposit_event(Event::<T>::StatementUpdated);
403			Ok(())
404		}
405
406		/// Set the block where locked DOTs will become unlocked.
407		///
408		/// Origin must match the `ConfigurationOrigin`
409		#[pallet::call_index(6)]
410		#[pallet::weight(T::DbWeight::get().writes(1))]
411		pub fn set_unlock_block(
412			origin: OriginFor<T>,
413			unlock_block: BlockNumberFor<T>,
414		) -> DispatchResult {
415			T::ConfigurationOrigin::ensure_origin(origin)?;
416			ensure!(
417				unlock_block > frame_system::Pallet::<T>::block_number(),
418				Error::<T>::InvalidUnlockBlock
419			);
420			// Possibly this is worse than having the caller account be the payment account?
421			UnlockBlock::<T>::set(unlock_block);
422			Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block });
423			Ok(())
424		}
425	}
426}
427
428impl<T: Config> Pallet<T> {
429	fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> {
430		// sr25519 always expects a 64 byte signature.
431		let signature: AnySignature = sr25519::Signature::try_from(signature)
432			.map_err(|_| Error::<T>::InvalidSignature)?
433			.into();
434
435		// In Polkadot, the AccountId is always the same as the 32 byte public key.
436		let account_bytes: [u8; 32] = account_to_bytes(who)?;
437		let public_key = sr25519::Public::from_raw(account_bytes);
438
439		let message = Statement::<T>::get();
440
441		// Check if everything is good or not.
442		match signature.verify(message.as_slice(), &public_key) {
443			true => Ok(()),
444			false => Err(Error::<T>::InvalidSignature)?,
445		}
446	}
447}
448
449// This function converts a 32 byte AccountId to its byte-array equivalent form.
450fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError>
451where
452	AccountId: Encode,
453{
454	let account_vec = account.encode();
455	ensure!(account_vec.len() == 32, "AccountId must be 32 bytes.");
456	let mut bytes = [0u8; 32];
457	bytes.copy_from_slice(&account_vec);
458	Ok(bytes)
459}
460
461/// WARNING: Executing this function will clear all storage used by this pallet.
462/// Be sure this is what you want...
463pub fn remove_pallet<T>() -> frame_support::weights::Weight
464where
465	T: frame_system::Config,
466{
467	#[allow(deprecated)]
468	use frame_support::migration::remove_storage_prefix;
469	#[allow(deprecated)]
470	remove_storage_prefix(b"Purchase", b"Accounts", b"");
471	#[allow(deprecated)]
472	remove_storage_prefix(b"Purchase", b"PaymentAccount", b"");
473	#[allow(deprecated)]
474	remove_storage_prefix(b"Purchase", b"Statement", b"");
475	#[allow(deprecated)]
476	remove_storage_prefix(b"Purchase", b"UnlockBlock", b"");
477
478	<T as frame_system::Config>::BlockWeights::get().max_block
479}
480
481#[cfg(test)]
482mod mock;
483
484#[cfg(test)]
485mod tests;