pezkuwi_runtime_common/purchase/
mod.rs

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