From 03e5db1fed6693c1695bb7aa910d84d8e302dc02 Mon Sep 17 00:00:00 2001 From: Ludovic_Domingues <ludovic.domingues96@gmail.com> Date: Thu, 19 Dec 2024 02:13:55 +0100 Subject: [PATCH] Improve pallet purchase file structure (#6780) Linked to issue #590 I moved the mod, tests, mock and benchmarking to their own seperate file to reduce the bloat inside purchase.rs --------- Co-authored-by: Guillaume Thiolliere <gui.thiolliere@gmail.com> Co-authored-by: Shawn Tabrizi <shawntabrizi@gmail.com> --- polkadot/runtime/common/src/purchase.rs | 1178 ----------------- polkadot/runtime/common/src/purchase/mock.rs | 181 +++ polkadot/runtime/common/src/purchase/mod.rs | 482 +++++++ polkadot/runtime/common/src/purchase/tests.rs | 547 ++++++++ 4 files changed, 1210 insertions(+), 1178 deletions(-) delete mode 100644 polkadot/runtime/common/src/purchase.rs create mode 100644 polkadot/runtime/common/src/purchase/mock.rs create mode 100644 polkadot/runtime/common/src/purchase/mod.rs create mode 100644 polkadot/runtime/common/src/purchase/tests.rs diff --git a/polkadot/runtime/common/src/purchase.rs b/polkadot/runtime/common/src/purchase.rs deleted file mode 100644 index cec92540654..00000000000 --- a/polkadot/runtime/common/src/purchase.rs +++ /dev/null @@ -1,1178 +0,0 @@ -// Copyright (C) Parity Technologies (UK) Ltd. -// 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/>. - -//! Pallet to process purchase of DOTs. - -use alloc::vec::Vec; -use codec::{Decode, Encode}; -use frame_support::{ - pallet_prelude::*, - traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule}, -}; -use frame_system::pallet_prelude::*; -pub use pallet::*; -use scale_info::TypeInfo; -use sp_core::sr25519; -use sp_runtime::{ - traits::{CheckedAdd, Saturating, Verify, Zero}, - AnySignature, DispatchError, DispatchResult, Permill, RuntimeDebug, -}; - -type BalanceOf<T> = - <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; - -/// The kind of statement an account needs to make for a claim to be valid. -#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, TypeInfo)] -pub enum AccountValidity { - /// Account is not valid. - Invalid, - /// Account has initiated the account creation process. - Initiated, - /// Account is pending validation. - Pending, - /// Account is valid with a low contribution amount. - ValidLow, - /// Account is valid with a high contribution amount. - ValidHigh, - /// Account has completed the purchase process. - Completed, -} - -impl Default for AccountValidity { - fn default() -> Self { - AccountValidity::Invalid - } -} - -impl AccountValidity { - fn is_valid(&self) -> bool { - match self { - Self::Invalid => false, - Self::Initiated => false, - Self::Pending => false, - Self::ValidLow => true, - Self::ValidHigh => true, - Self::Completed => false, - } - } -} - -/// All information about an account regarding the purchase of DOTs. -#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)] -pub struct AccountStatus<Balance> { - /// The current validity status of the user. Will denote if the user has passed KYC, - /// how much they are able to purchase, and when their purchase process has completed. - validity: AccountValidity, - /// The amount of free DOTs they have purchased. - free_balance: Balance, - /// The amount of locked DOTs they have purchased. - locked_balance: Balance, - /// Their sr25519/ed25519 signature verifying they have signed our required statement. - signature: Vec<u8>, - /// The percentage of VAT the purchaser is responsible for. This is already factored into - /// account balance. - vat: Permill, -} - -#[frame_support::pallet] -pub mod pallet { - use super::*; - - #[pallet::pallet] - #[pallet::without_storage_info] - pub struct Pallet<T>(_); - - #[pallet::config] - pub trait Config: frame_system::Config { - /// The overarching event type. - type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; - - /// Balances Pallet - type Currency: Currency<Self::AccountId>; - - /// Vesting Pallet - type VestingSchedule: VestingSchedule< - Self::AccountId, - Moment = BlockNumberFor<Self>, - Currency = Self::Currency, - >; - - /// The origin allowed to set account status. - type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>; - - /// The origin allowed to make configurations to the pallet. - type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>; - - /// The maximum statement length for the statement users to sign when creating an account. - #[pallet::constant] - type MaxStatementLength: Get<u32>; - - /// The amount of purchased locked DOTs that we will unlock for basic actions on the chain. - #[pallet::constant] - type UnlockedProportion: Get<Permill>; - - /// The maximum amount of locked DOTs that we will unlock. - #[pallet::constant] - type MaxUnlocked: Get<BalanceOf<Self>>; - } - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event<T: Config> { - /// A new account was created. - AccountCreated { who: T::AccountId }, - /// Someone's account validity was updated. - ValidityUpdated { who: T::AccountId, validity: AccountValidity }, - /// Someone's purchase balance was updated. - BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> }, - /// A payout was made to a purchaser. - PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> }, - /// A new payment account was set. - PaymentAccountSet { who: T::AccountId }, - /// A new statement was set. - StatementUpdated, - /// A new statement was set. `[block_number]` - UnlockBlockUpdated { block_number: BlockNumberFor<T> }, - } - - #[pallet::error] - pub enum Error<T> { - /// Account is not currently valid to use. - InvalidAccount, - /// Account used in the purchase already exists. - ExistingAccount, - /// Provided signature is invalid - InvalidSignature, - /// Account has already completed the purchase process. - AlreadyCompleted, - /// An overflow occurred when doing calculations. - Overflow, - /// The statement is too long to be stored on chain. - InvalidStatement, - /// The unlock block is in the past! - InvalidUnlockBlock, - /// Vesting schedule already exists for this account. - VestingScheduleExists, - } - - // A map of all participants in the DOT purchase process. - #[pallet::storage] - pub(super) type Accounts<T: Config> = - StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>; - - // The account that will be used to payout participants of the DOT purchase process. - #[pallet::storage] - pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>; - - // The statement purchasers will need to sign to participate. - #[pallet::storage] - pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>; - - // The block where all locked dots will unlock. - #[pallet::storage] - pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>; - - #[pallet::hooks] - impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {} - - #[pallet::call] - impl<T: Config> Pallet<T> { - /// Create a new account. Proof of existence through a valid signed message. - /// - /// We check that the account does not exist at this stage. - /// - /// Origin must match the `ValidityOrigin`. - #[pallet::call_index(0)] - #[pallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))] - pub fn create_account( - origin: OriginFor<T>, - who: T::AccountId, - signature: Vec<u8>, - ) -> DispatchResult { - T::ValidityOrigin::ensure_origin(origin)?; - // Account is already being tracked by the pallet. - ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount); - // Account should not have a vesting schedule. - ensure!( - T::VestingSchedule::vesting_balance(&who).is_none(), - Error::<T>::VestingScheduleExists - ); - - // Verify the signature provided is valid for the statement. - Self::verify_signature(&who, &signature)?; - - // Create a new pending account. - let status = AccountStatus { - validity: AccountValidity::Initiated, - signature, - free_balance: Zero::zero(), - locked_balance: Zero::zero(), - vat: Permill::zero(), - }; - Accounts::<T>::insert(&who, status); - Self::deposit_event(Event::<T>::AccountCreated { who }); - Ok(()) - } - - /// Update the validity status of an existing account. If set to completed, the account - /// will no longer be able to continue through the crowdfund process. - /// - /// We check that the account exists at this stage, but has not completed the process. - /// - /// Origin must match the `ValidityOrigin`. - #[pallet::call_index(1)] - #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] - pub fn update_validity_status( - origin: OriginFor<T>, - who: T::AccountId, - validity: AccountValidity, - ) -> DispatchResult { - T::ValidityOrigin::ensure_origin(origin)?; - ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount); - Accounts::<T>::try_mutate( - &who, - |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult { - ensure!( - status.validity != AccountValidity::Completed, - Error::<T>::AlreadyCompleted - ); - status.validity = validity; - Ok(()) - }, - )?; - Self::deposit_event(Event::<T>::ValidityUpdated { who, validity }); - Ok(()) - } - - /// Update the balance of a valid account. - /// - /// We check that the account is valid for a balance transfer at this point. - /// - /// Origin must match the `ValidityOrigin`. - #[pallet::call_index(2)] - #[pallet::weight(T::DbWeight::get().reads_writes(2, 1))] - pub fn update_balance( - origin: OriginFor<T>, - who: T::AccountId, - free_balance: BalanceOf<T>, - locked_balance: BalanceOf<T>, - vat: Permill, - ) -> DispatchResult { - T::ValidityOrigin::ensure_origin(origin)?; - - Accounts::<T>::try_mutate( - &who, - |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult { - // Account has a valid status (not Invalid, Pending, or Completed)... - ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount); - - free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?; - status.free_balance = free_balance; - status.locked_balance = locked_balance; - status.vat = vat; - Ok(()) - }, - )?; - Self::deposit_event(Event::<T>::BalanceUpdated { - who, - free: free_balance, - locked: locked_balance, - }); - Ok(()) - } - - /// Pay the user and complete the purchase process. - /// - /// We reverify all assumptions about the state of an account, and complete the process. - /// - /// Origin must match the configured `PaymentAccount` (if it is not configured then this - /// will always fail with `BadOrigin`). - #[pallet::call_index(3)] - #[pallet::weight(T::DbWeight::get().reads_writes(4, 2))] - pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult { - // Payments must be made directly by the `PaymentAccount`. - let payment_account = ensure_signed(origin)?; - let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?; - ensure!(payment_account == test_against, DispatchError::BadOrigin); - - // Account should not have a vesting schedule. - ensure!( - T::VestingSchedule::vesting_balance(&who).is_none(), - Error::<T>::VestingScheduleExists - ); - - Accounts::<T>::try_mutate( - &who, - |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult { - // Account has a valid status (not Invalid, Pending, or Completed)... - ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount); - - // Transfer funds from the payment account into the purchasing user. - let total_balance = status - .free_balance - .checked_add(&status.locked_balance) - .ok_or(Error::<T>::Overflow)?; - T::Currency::transfer( - &payment_account, - &who, - total_balance, - ExistenceRequirement::AllowDeath, - )?; - - if !status.locked_balance.is_zero() { - let unlock_block = UnlockBlock::<T>::get(); - // We allow some configurable portion of the purchased locked DOTs to be - // unlocked for basic usage. - let unlocked = (T::UnlockedProportion::get() * status.locked_balance) - .min(T::MaxUnlocked::get()); - let locked = status.locked_balance.saturating_sub(unlocked); - // We checked that this account has no existing vesting schedule. So this - // function should never fail, however if it does, not much we can do about - // it at this point. - let _ = T::VestingSchedule::add_vesting_schedule( - // Apply vesting schedule to this user - &who, - // For this much amount - locked, - // Unlocking the full amount after one block - locked, - // When everything unlocks - unlock_block, - ); - } - - // Setting the user account to `Completed` ends the purchase process for this - // user. - status.validity = AccountValidity::Completed; - Self::deposit_event(Event::<T>::PaymentComplete { - who: who.clone(), - free: status.free_balance, - locked: status.locked_balance, - }); - Ok(()) - }, - )?; - Ok(()) - } - - /* Configuration Operations */ - - /// Set the account that will be used to payout users in the DOT purchase process. - /// - /// Origin must match the `ConfigurationOrigin` - #[pallet::call_index(4)] - #[pallet::weight(T::DbWeight::get().writes(1))] - pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult { - T::ConfigurationOrigin::ensure_origin(origin)?; - // Possibly this is worse than having the caller account be the payment account? - PaymentAccount::<T>::put(who.clone()); - Self::deposit_event(Event::<T>::PaymentAccountSet { who }); - Ok(()) - } - - /// Set the statement that must be signed for a user to participate on the DOT sale. - /// - /// Origin must match the `ConfigurationOrigin` - #[pallet::call_index(5)] - #[pallet::weight(T::DbWeight::get().writes(1))] - pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult { - T::ConfigurationOrigin::ensure_origin(origin)?; - ensure!( - (statement.len() as u32) < T::MaxStatementLength::get(), - Error::<T>::InvalidStatement - ); - // Possibly this is worse than having the caller account be the payment account? - Statement::<T>::set(statement); - Self::deposit_event(Event::<T>::StatementUpdated); - Ok(()) - } - - /// Set the block where locked DOTs will become unlocked. - /// - /// Origin must match the `ConfigurationOrigin` - #[pallet::call_index(6)] - #[pallet::weight(T::DbWeight::get().writes(1))] - pub fn set_unlock_block( - origin: OriginFor<T>, - unlock_block: BlockNumberFor<T>, - ) -> DispatchResult { - T::ConfigurationOrigin::ensure_origin(origin)?; - ensure!( - unlock_block > frame_system::Pallet::<T>::block_number(), - Error::<T>::InvalidUnlockBlock - ); - // Possibly this is worse than having the caller account be the payment account? - UnlockBlock::<T>::set(unlock_block); - Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block }); - Ok(()) - } - } -} - -impl<T: Config> Pallet<T> { - fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> { - // sr25519 always expects a 64 byte signature. - let signature: AnySignature = sr25519::Signature::try_from(signature) - .map_err(|_| Error::<T>::InvalidSignature)? - .into(); - - // In Polkadot, the AccountId is always the same as the 32 byte public key. - let account_bytes: [u8; 32] = account_to_bytes(who)?; - let public_key = sr25519::Public::from_raw(account_bytes); - - let message = Statement::<T>::get(); - - // Check if everything is good or not. - match signature.verify(message.as_slice(), &public_key) { - true => Ok(()), - false => Err(Error::<T>::InvalidSignature)?, - } - } -} - -// This function converts a 32 byte AccountId to its byte-array equivalent form. -fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError> -where - AccountId: Encode, -{ - let account_vec = account.encode(); - ensure!(account_vec.len() == 32, "AccountId must be 32 bytes."); - let mut bytes = [0u8; 32]; - bytes.copy_from_slice(&account_vec); - Ok(bytes) -} - -/// WARNING: Executing this function will clear all storage used by this pallet. -/// Be sure this is what you want... -pub fn remove_pallet<T>() -> frame_support::weights::Weight -where - T: frame_system::Config, -{ - #[allow(deprecated)] - use frame_support::migration::remove_storage_prefix; - #[allow(deprecated)] - remove_storage_prefix(b"Purchase", b"Accounts", b""); - #[allow(deprecated)] - remove_storage_prefix(b"Purchase", b"PaymentAccount", b""); - #[allow(deprecated)] - remove_storage_prefix(b"Purchase", b"Statement", b""); - #[allow(deprecated)] - remove_storage_prefix(b"Purchase", b"UnlockBlock", b""); - - <T as frame_system::Config>::BlockWeights::get().max_block -} - -#[cfg(test)] -mod tests { - use super::*; - - use sp_core::{crypto::AccountId32, H256}; - use sp_keyring::{Ed25519Keyring, Sr25519Keyring}; - // 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 required. - use crate::purchase; - use frame_support::{ - assert_noop, assert_ok, derive_impl, ord_parameter_types, parameter_types, - traits::{Currency, WithdrawReasons}, - }; - use sp_runtime::{ - traits::{BlakeTwo256, Dispatchable, Identity, IdentityLookup}, - ArithmeticError, BuildStorage, - DispatchError::BadOrigin, - }; - - type Block = frame_system::mocking::MockBlock<Test>; - - frame_support::construct_runtime!( - pub enum Test - { - System: frame_system, - Balances: pallet_balances, - Vesting: pallet_vesting, - Purchase: purchase, - } - ); - - type AccountId = AccountId32; - - #[derive_impl(frame_system::config_preludes::TestDefaultConfig)] - impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type RuntimeCall = RuntimeCall; - type Nonce = u64; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = AccountId; - type Lookup = IdentityLookup<AccountId>; - type Block = Block; - type RuntimeEvent = RuntimeEvent; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData<u64>; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; - } - - #[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] - impl pallet_balances::Config for Test { - type AccountStore = System; - } - - parameter_types! { - pub const MinVestedTransfer: u64 = 1; - pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = - WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); - } - - impl pallet_vesting::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type BlockNumberToBalance = Identity; - type MinVestedTransfer = MinVestedTransfer; - type WeightInfo = (); - type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; - type BlockNumberProvider = System; - const MAX_VESTING_SCHEDULES: u32 = 28; - } - - parameter_types! { - pub const MaxStatementLength: u32 = 1_000; - pub const UnlockedProportion: Permill = Permill::from_percent(10); - pub const MaxUnlocked: u64 = 10; - } - - ord_parameter_types! { - pub const ValidityOrigin: AccountId = AccountId32::from([0u8; 32]); - pub const PaymentOrigin: AccountId = AccountId32::from([1u8; 32]); - pub const ConfigurationOrigin: AccountId = AccountId32::from([2u8; 32]); - } - - impl Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type VestingSchedule = Vesting; - type ValidityOrigin = frame_system::EnsureSignedBy<ValidityOrigin, AccountId>; - type ConfigurationOrigin = frame_system::EnsureSignedBy<ConfigurationOrigin, AccountId>; - type MaxStatementLength = MaxStatementLength; - type UnlockedProportion = UnlockedProportion; - type MaxUnlocked = MaxUnlocked; - } - - // This function basically just builds a genesis storage key/value store according to - // our desired mockup. It also executes our `setup` function which sets up this pallet for use. - pub fn new_test_ext() -> sp_io::TestExternalities { - let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap(); - let mut ext = sp_io::TestExternalities::new(t); - ext.execute_with(|| setup()); - ext - } - - fn setup() { - let statement = b"Hello, World".to_vec(); - let unlock_block = 100; - Purchase::set_statement(RuntimeOrigin::signed(configuration_origin()), statement).unwrap(); - Purchase::set_unlock_block(RuntimeOrigin::signed(configuration_origin()), unlock_block) - .unwrap(); - Purchase::set_payment_account( - RuntimeOrigin::signed(configuration_origin()), - payment_account(), - ) - .unwrap(); - Balances::make_free_balance_be(&payment_account(), 100_000); - } - - fn alice() -> AccountId { - Sr25519Keyring::Alice.to_account_id() - } - - fn alice_ed25519() -> AccountId { - Ed25519Keyring::Alice.to_account_id() - } - - fn bob() -> AccountId { - Sr25519Keyring::Bob.to_account_id() - } - - fn alice_signature() -> [u8; 64] { - // echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold - // race lonely fit walk//Alice" - hex_literal::hex!("20e0faffdf4dfe939f2faa560f73b1d01cde8472e2b690b7b40606a374244c3a2e9eb9c8107c10b605138374003af8819bd4387d7c24a66ee9253c2e688ab881") - } - - fn bob_signature() -> [u8; 64] { - // echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold - // race lonely fit walk//Bob" - hex_literal::hex!("d6d460187ecf530f3ec2d6e3ac91b9d083c8fbd8f1112d92a82e4d84df552d18d338e6da8944eba6e84afaacf8a9850f54e7b53a84530d649be2e0119c7ce889") - } - - fn alice_signature_ed25519() -> [u8; 64] { - // echo -n "Hello, World" | subkey -e sign "bottom drive obey lake curtain smoke basket hold - // race lonely fit walk//Alice" - hex_literal::hex!("ee3f5a6cbfc12a8f00c18b811dc921b550ddf272354cda4b9a57b1d06213fcd8509f5af18425d39a279d13622f14806c3e978e2163981f2ec1c06e9628460b0e") - } - - fn validity_origin() -> AccountId { - ValidityOrigin::get() - } - - fn configuration_origin() -> AccountId { - ConfigurationOrigin::get() - } - - fn payment_account() -> AccountId { - [42u8; 32].into() - } - - #[test] - fn set_statement_works_and_handles_basic_errors() { - new_test_ext().execute_with(|| { - let statement = b"Test Set Statement".to_vec(); - // Invalid origin - assert_noop!( - Purchase::set_statement(RuntimeOrigin::signed(alice()), statement.clone()), - BadOrigin, - ); - // Too Long - let long_statement = [0u8; 10_000].to_vec(); - assert_noop!( - Purchase::set_statement( - RuntimeOrigin::signed(configuration_origin()), - long_statement - ), - Error::<Test>::InvalidStatement, - ); - // Just right... - assert_ok!(Purchase::set_statement( - RuntimeOrigin::signed(configuration_origin()), - statement.clone() - )); - assert_eq!(Statement::<Test>::get(), statement); - }); - } - - #[test] - fn set_unlock_block_works_and_handles_basic_errors() { - new_test_ext().execute_with(|| { - let unlock_block = 69; - // Invalid origin - assert_noop!( - Purchase::set_unlock_block(RuntimeOrigin::signed(alice()), unlock_block), - BadOrigin, - ); - // Block Number in Past - let bad_unlock_block = 50; - System::set_block_number(bad_unlock_block); - assert_noop!( - Purchase::set_unlock_block( - RuntimeOrigin::signed(configuration_origin()), - bad_unlock_block - ), - Error::<Test>::InvalidUnlockBlock, - ); - // Just right... - assert_ok!(Purchase::set_unlock_block( - RuntimeOrigin::signed(configuration_origin()), - unlock_block - )); - assert_eq!(UnlockBlock::<Test>::get(), unlock_block); - }); - } - - #[test] - fn set_payment_account_works_and_handles_basic_errors() { - new_test_ext().execute_with(|| { - let payment_account: AccountId = [69u8; 32].into(); - // Invalid Origin - assert_noop!( - Purchase::set_payment_account( - RuntimeOrigin::signed(alice()), - payment_account.clone() - ), - BadOrigin, - ); - // Just right... - assert_ok!(Purchase::set_payment_account( - RuntimeOrigin::signed(configuration_origin()), - payment_account.clone() - )); - assert_eq!(PaymentAccount::<Test>::get(), Some(payment_account)); - }); - } - - #[test] - fn signature_verification_works() { - new_test_ext().execute_with(|| { - assert_ok!(Purchase::verify_signature(&alice(), &alice_signature())); - assert_ok!(Purchase::verify_signature(&alice_ed25519(), &alice_signature_ed25519())); - assert_ok!(Purchase::verify_signature(&bob(), &bob_signature())); - - // Mixing and matching fails - assert_noop!( - Purchase::verify_signature(&alice(), &bob_signature()), - Error::<Test>::InvalidSignature - ); - assert_noop!( - Purchase::verify_signature(&bob(), &alice_signature()), - Error::<Test>::InvalidSignature - ); - }); - } - - #[test] - fn account_creation_works() { - new_test_ext().execute_with(|| { - assert!(!Accounts::<Test>::contains_key(alice())); - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec(), - )); - assert_eq!( - Accounts::<Test>::get(alice()), - AccountStatus { - validity: AccountValidity::Initiated, - free_balance: Zero::zero(), - locked_balance: Zero::zero(), - signature: alice_signature().to_vec(), - vat: Permill::zero(), - } - ); - }); - } - - #[test] - fn account_creation_handles_basic_errors() { - new_test_ext().execute_with(|| { - // Wrong Origin - assert_noop!( - Purchase::create_account( - RuntimeOrigin::signed(alice()), - alice(), - alice_signature().to_vec() - ), - BadOrigin, - ); - - // Wrong Account/Signature - assert_noop!( - Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - bob_signature().to_vec() - ), - Error::<Test>::InvalidSignature, - ); - - // Account with vesting - Balances::make_free_balance_be(&alice(), 100); - assert_ok!(<Test as Config>::VestingSchedule::add_vesting_schedule( - &alice(), - 100, - 1, - 50 - )); - assert_noop!( - Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec() - ), - Error::<Test>::VestingScheduleExists, - ); - - // Duplicate Purchasing Account - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - bob(), - bob_signature().to_vec() - )); - assert_noop!( - Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - bob(), - bob_signature().to_vec() - ), - Error::<Test>::ExistingAccount, - ); - }); - } - - #[test] - fn update_validity_status_works() { - new_test_ext().execute_with(|| { - // Alice account is created. - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec(), - )); - // She submits KYC, and we update the status to `Pending`. - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::Pending, - )); - // KYC comes back negative, so we mark the account invalid. - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::Invalid, - )); - assert_eq!( - Accounts::<Test>::get(alice()), - AccountStatus { - validity: AccountValidity::Invalid, - free_balance: Zero::zero(), - locked_balance: Zero::zero(), - signature: alice_signature().to_vec(), - vat: Permill::zero(), - } - ); - // She fixes it, we mark her account valid. - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::ValidLow, - )); - assert_eq!( - Accounts::<Test>::get(alice()), - AccountStatus { - validity: AccountValidity::ValidLow, - free_balance: Zero::zero(), - locked_balance: Zero::zero(), - signature: alice_signature().to_vec(), - vat: Permill::zero(), - } - ); - }); - } - - #[test] - fn update_validity_status_handles_basic_errors() { - new_test_ext().execute_with(|| { - // Wrong Origin - assert_noop!( - Purchase::update_validity_status( - RuntimeOrigin::signed(alice()), - alice(), - AccountValidity::Pending, - ), - BadOrigin - ); - // Inactive Account - assert_noop!( - Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::Pending, - ), - Error::<Test>::InvalidAccount - ); - // Already Completed - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec(), - )); - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::Completed, - )); - assert_noop!( - Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::Pending, - ), - Error::<Test>::AlreadyCompleted - ); - }); - } - - #[test] - fn update_balance_works() { - new_test_ext().execute_with(|| { - // Alice account is created - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec() - )); - // And approved for basic contribution - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::ValidLow, - )); - // We set a balance on the user based on the payment they made. 50 locked, 50 free. - assert_ok!(Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - alice(), - 50, - 50, - Permill::from_rational(77u32, 1000u32), - )); - assert_eq!( - Accounts::<Test>::get(alice()), - AccountStatus { - validity: AccountValidity::ValidLow, - free_balance: 50, - locked_balance: 50, - signature: alice_signature().to_vec(), - vat: Permill::from_parts(77000), - } - ); - // We can update the balance based on new information. - assert_ok!(Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - alice(), - 25, - 50, - Permill::zero(), - )); - assert_eq!( - Accounts::<Test>::get(alice()), - AccountStatus { - validity: AccountValidity::ValidLow, - free_balance: 25, - locked_balance: 50, - signature: alice_signature().to_vec(), - vat: Permill::zero(), - } - ); - }); - } - - #[test] - fn update_balance_handles_basic_errors() { - new_test_ext().execute_with(|| { - // Wrong Origin - assert_noop!( - Purchase::update_balance( - RuntimeOrigin::signed(alice()), - alice(), - 50, - 50, - Permill::zero(), - ), - BadOrigin - ); - // Inactive Account - assert_noop!( - Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - alice(), - 50, - 50, - Permill::zero(), - ), - Error::<Test>::InvalidAccount - ); - // Overflow - assert_noop!( - Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - alice(), - u64::MAX, - u64::MAX, - Permill::zero(), - ), - Error::<Test>::InvalidAccount - ); - }); - } - - #[test] - fn payout_works() { - new_test_ext().execute_with(|| { - // Alice and Bob accounts are created - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec() - )); - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - bob(), - bob_signature().to_vec() - )); - // Alice is approved for basic contribution - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::ValidLow, - )); - // Bob is approved for high contribution - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - bob(), - AccountValidity::ValidHigh, - )); - // We set a balance on the users based on the payment they made. 50 locked, 50 free. - assert_ok!(Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - alice(), - 50, - 50, - Permill::zero(), - )); - assert_ok!(Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - bob(), - 100, - 150, - Permill::zero(), - )); - // Now we call payout for Alice and Bob. - assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),)); - assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),)); - // Payment is made. - assert_eq!(<Test as Config>::Currency::free_balance(&payment_account()), 99_650); - assert_eq!(<Test as Config>::Currency::free_balance(&alice()), 100); - // 10% of the 50 units is unlocked automatically for Alice - assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45)); - assert_eq!(<Test as Config>::Currency::free_balance(&bob()), 250); - // A max of 10 units is unlocked automatically for Bob - assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140)); - // Status is completed. - assert_eq!( - Accounts::<Test>::get(alice()), - AccountStatus { - validity: AccountValidity::Completed, - free_balance: 50, - locked_balance: 50, - signature: alice_signature().to_vec(), - vat: Permill::zero(), - } - ); - assert_eq!( - Accounts::<Test>::get(bob()), - AccountStatus { - validity: AccountValidity::Completed, - free_balance: 100, - locked_balance: 150, - signature: bob_signature().to_vec(), - vat: Permill::zero(), - } - ); - // Vesting lock is removed in whole on block 101 (100 blocks after block 1) - System::set_block_number(100); - let vest_call = RuntimeCall::Vesting(pallet_vesting::Call::<Test>::vest {}); - assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice()))); - assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob()))); - assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45)); - assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140)); - System::set_block_number(101); - assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice()))); - assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob()))); - assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), None); - assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), None); - }); - } - - #[test] - fn payout_handles_basic_errors() { - new_test_ext().execute_with(|| { - // Wrong Origin - assert_noop!(Purchase::payout(RuntimeOrigin::signed(alice()), alice(),), BadOrigin); - // Account with Existing Vesting Schedule - Balances::make_free_balance_be(&bob(), 100); - assert_ok!( - <Test as Config>::VestingSchedule::add_vesting_schedule(&bob(), 100, 1, 50,) - ); - assert_noop!( - Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),), - Error::<Test>::VestingScheduleExists - ); - // Invalid Account (never created) - assert_noop!( - Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),), - Error::<Test>::InvalidAccount - ); - // Invalid Account (created, but not valid) - assert_ok!(Purchase::create_account( - RuntimeOrigin::signed(validity_origin()), - alice(), - alice_signature().to_vec() - )); - assert_noop!( - Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),), - Error::<Test>::InvalidAccount - ); - // Not enough funds in payment account - assert_ok!(Purchase::update_validity_status( - RuntimeOrigin::signed(validity_origin()), - alice(), - AccountValidity::ValidHigh, - )); - assert_ok!(Purchase::update_balance( - RuntimeOrigin::signed(validity_origin()), - alice(), - 100_000, - 100_000, - Permill::zero(), - )); - assert_noop!( - Purchase::payout(RuntimeOrigin::signed(payment_account()), alice()), - ArithmeticError::Underflow - ); - }); - } - - #[test] - fn remove_pallet_works() { - new_test_ext().execute_with(|| { - let account_status = AccountStatus { - validity: AccountValidity::Completed, - free_balance: 1234, - locked_balance: 4321, - signature: b"my signature".to_vec(), - vat: Permill::from_percent(50), - }; - - // Add some storage. - Accounts::<Test>::insert(alice(), account_status.clone()); - Accounts::<Test>::insert(bob(), account_status); - PaymentAccount::<Test>::put(alice()); - Statement::<Test>::put(b"hello, world!".to_vec()); - UnlockBlock::<Test>::put(4); - - // Verify storage exists. - assert_eq!(Accounts::<Test>::iter().count(), 2); - assert!(PaymentAccount::<Test>::exists()); - assert!(Statement::<Test>::exists()); - assert!(UnlockBlock::<Test>::exists()); - - // Remove storage. - remove_pallet::<Test>(); - - // Verify storage is gone. - assert_eq!(Accounts::<Test>::iter().count(), 0); - assert!(!PaymentAccount::<Test>::exists()); - assert!(!Statement::<Test>::exists()); - assert!(!UnlockBlock::<Test>::exists()); - }); - } -} diff --git a/polkadot/runtime/common/src/purchase/mock.rs b/polkadot/runtime/common/src/purchase/mock.rs new file mode 100644 index 00000000000..ec8599f3b79 --- /dev/null +++ b/polkadot/runtime/common/src/purchase/mock.rs @@ -0,0 +1,181 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// 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/>. + +//! Mocking utilities for testing in purchase pallet. + +#[cfg(test)] +use super::*; + +use sp_core::{crypto::AccountId32, H256}; +use sp_keyring::{Ed25519Keyring, Sr25519Keyring}; +// 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 required. +use crate::purchase; +use frame_support::{ + derive_impl, ord_parameter_types, parameter_types, + traits::{Currency, WithdrawReasons}, +}; +use sp_runtime::{ + traits::{BlakeTwo256, Identity, IdentityLookup}, + BuildStorage, +}; + +type Block = frame_system::mocking::MockBlock<Test>; + +frame_support::construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + Vesting: pallet_vesting, + Purchase: purchase, + } +); + +type AccountId = AccountId32; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type Nonce = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup<AccountId>; + type Block = Block; + type RuntimeEvent = RuntimeEvent; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData<u64>; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +parameter_types! { + pub const MinVestedTransfer: u64 = 1; + pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = + WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); +} + +impl pallet_vesting::Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type BlockNumberToBalance = Identity; + type MinVestedTransfer = MinVestedTransfer; + type WeightInfo = (); + type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; + type BlockNumberProvider = System; + const MAX_VESTING_SCHEDULES: u32 = 28; +} + +parameter_types! { + pub const MaxStatementLength: u32 = 1_000; + pub const UnlockedProportion: Permill = Permill::from_percent(10); + pub const MaxUnlocked: u64 = 10; +} + +ord_parameter_types! { + pub const ValidityOrigin: AccountId = AccountId32::from([0u8; 32]); + pub const PaymentOrigin: AccountId = AccountId32::from([1u8; 32]); + pub const ConfigurationOrigin: AccountId = AccountId32::from([2u8; 32]); +} + +impl Config for Test { + type RuntimeEvent = RuntimeEvent; + type Currency = Balances; + type VestingSchedule = Vesting; + type ValidityOrigin = frame_system::EnsureSignedBy<ValidityOrigin, AccountId>; + type ConfigurationOrigin = frame_system::EnsureSignedBy<ConfigurationOrigin, AccountId>; + type MaxStatementLength = MaxStatementLength; + type UnlockedProportion = UnlockedProportion; + type MaxUnlocked = MaxUnlocked; +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. It also executes our `setup` function which sets up this pallet for use. +pub fn new_test_ext() -> sp_io::TestExternalities { + let t = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(|| setup()); + ext +} + +pub fn setup() { + let statement = b"Hello, World".to_vec(); + let unlock_block = 100; + Purchase::set_statement(RuntimeOrigin::signed(configuration_origin()), statement).unwrap(); + Purchase::set_unlock_block(RuntimeOrigin::signed(configuration_origin()), unlock_block) + .unwrap(); + Purchase::set_payment_account(RuntimeOrigin::signed(configuration_origin()), payment_account()) + .unwrap(); + Balances::make_free_balance_be(&payment_account(), 100_000); +} + +pub fn alice() -> AccountId { + Sr25519Keyring::Alice.to_account_id() +} + +pub fn alice_ed25519() -> AccountId { + Ed25519Keyring::Alice.to_account_id() +} + +pub fn bob() -> AccountId { + Sr25519Keyring::Bob.to_account_id() +} + +pub fn alice_signature() -> [u8; 64] { + // echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold + // race lonely fit walk//Alice" + hex_literal::hex!("20e0faffdf4dfe939f2faa560f73b1d01cde8472e2b690b7b40606a374244c3a2e9eb9c8107c10b605138374003af8819bd4387d7c24a66ee9253c2e688ab881") +} + +pub fn bob_signature() -> [u8; 64] { + // echo -n "Hello, World" | subkey -s sign "bottom drive obey lake curtain smoke basket hold + // race lonely fit walk//Bob" + hex_literal::hex!("d6d460187ecf530f3ec2d6e3ac91b9d083c8fbd8f1112d92a82e4d84df552d18d338e6da8944eba6e84afaacf8a9850f54e7b53a84530d649be2e0119c7ce889") +} + +pub fn alice_signature_ed25519() -> [u8; 64] { + // echo -n "Hello, World" | subkey -e sign "bottom drive obey lake curtain smoke basket hold + // race lonely fit walk//Alice" + hex_literal::hex!("ee3f5a6cbfc12a8f00c18b811dc921b550ddf272354cda4b9a57b1d06213fcd8509f5af18425d39a279d13622f14806c3e978e2163981f2ec1c06e9628460b0e") +} + +pub fn validity_origin() -> AccountId { + ValidityOrigin::get() +} + +pub fn configuration_origin() -> AccountId { + ConfigurationOrigin::get() +} + +pub fn payment_account() -> AccountId { + [42u8; 32].into() +} diff --git a/polkadot/runtime/common/src/purchase/mod.rs b/polkadot/runtime/common/src/purchase/mod.rs new file mode 100644 index 00000000000..71dc5b57967 --- /dev/null +++ b/polkadot/runtime/common/src/purchase/mod.rs @@ -0,0 +1,482 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// 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/>. + +//! Pallet to process purchase of DOTs. + +use alloc::vec::Vec; +use codec::{Decode, Encode}; +use frame_support::{ + pallet_prelude::*, + traits::{Currency, EnsureOrigin, ExistenceRequirement, Get, VestingSchedule}, +}; +use frame_system::pallet_prelude::*; +pub use pallet::*; +use scale_info::TypeInfo; +use sp_core::sr25519; +use sp_runtime::{ + traits::{CheckedAdd, Saturating, Verify, Zero}, + AnySignature, DispatchError, DispatchResult, Permill, RuntimeDebug, +}; + +type BalanceOf<T> = + <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance; + +/// The kind of statement an account needs to make for a claim to be valid. +#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub enum AccountValidity { + /// Account is not valid. + Invalid, + /// Account has initiated the account creation process. + Initiated, + /// Account is pending validation. + Pending, + /// Account is valid with a low contribution amount. + ValidLow, + /// Account is valid with a high contribution amount. + ValidHigh, + /// Account has completed the purchase process. + Completed, +} + +impl Default for AccountValidity { + fn default() -> Self { + AccountValidity::Invalid + } +} + +impl AccountValidity { + fn is_valid(&self) -> bool { + match self { + Self::Invalid => false, + Self::Initiated => false, + Self::Pending => false, + Self::ValidLow => true, + Self::ValidHigh => true, + Self::Completed => false, + } + } +} + +/// All information about an account regarding the purchase of DOTs. +#[derive(Encode, Decode, Default, Clone, Eq, PartialEq, RuntimeDebug, TypeInfo)] +pub struct AccountStatus<Balance> { + /// The current validity status of the user. Will denote if the user has passed KYC, + /// how much they are able to purchase, and when their purchase process has completed. + validity: AccountValidity, + /// The amount of free DOTs they have purchased. + free_balance: Balance, + /// The amount of locked DOTs they have purchased. + locked_balance: Balance, + /// Their sr25519/ed25519 signature verifying they have signed our required statement. + signature: Vec<u8>, + /// The percentage of VAT the purchaser is responsible for. This is already factored into + /// account balance. + vat: Permill, +} + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet<T>(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>; + + /// Balances Pallet + type Currency: Currency<Self::AccountId>; + + /// Vesting Pallet + type VestingSchedule: VestingSchedule< + Self::AccountId, + Moment = BlockNumberFor<Self>, + Currency = Self::Currency, + >; + + /// The origin allowed to set account status. + type ValidityOrigin: EnsureOrigin<Self::RuntimeOrigin>; + + /// The origin allowed to make configurations to the pallet. + type ConfigurationOrigin: EnsureOrigin<Self::RuntimeOrigin>; + + /// The maximum statement length for the statement users to sign when creating an account. + #[pallet::constant] + type MaxStatementLength: Get<u32>; + + /// The amount of purchased locked DOTs that we will unlock for basic actions on the chain. + #[pallet::constant] + type UnlockedProportion: Get<Permill>; + + /// The maximum amount of locked DOTs that we will unlock. + #[pallet::constant] + type MaxUnlocked: Get<BalanceOf<Self>>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event<T: Config> { + /// A new account was created. + AccountCreated { who: T::AccountId }, + /// Someone's account validity was updated. + ValidityUpdated { who: T::AccountId, validity: AccountValidity }, + /// Someone's purchase balance was updated. + BalanceUpdated { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> }, + /// A payout was made to a purchaser. + PaymentComplete { who: T::AccountId, free: BalanceOf<T>, locked: BalanceOf<T> }, + /// A new payment account was set. + PaymentAccountSet { who: T::AccountId }, + /// A new statement was set. + StatementUpdated, + /// A new statement was set. `[block_number]` + UnlockBlockUpdated { block_number: BlockNumberFor<T> }, + } + + #[pallet::error] + pub enum Error<T> { + /// Account is not currently valid to use. + InvalidAccount, + /// Account used in the purchase already exists. + ExistingAccount, + /// Provided signature is invalid + InvalidSignature, + /// Account has already completed the purchase process. + AlreadyCompleted, + /// An overflow occurred when doing calculations. + Overflow, + /// The statement is too long to be stored on chain. + InvalidStatement, + /// The unlock block is in the past! + InvalidUnlockBlock, + /// Vesting schedule already exists for this account. + VestingScheduleExists, + } + + // A map of all participants in the DOT purchase process. + #[pallet::storage] + pub(super) type Accounts<T: Config> = + StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus<BalanceOf<T>>, ValueQuery>; + + // The account that will be used to payout participants of the DOT purchase process. + #[pallet::storage] + pub(super) type PaymentAccount<T: Config> = StorageValue<_, T::AccountId, OptionQuery>; + + // The statement purchasers will need to sign to participate. + #[pallet::storage] + pub(super) type Statement<T> = StorageValue<_, Vec<u8>, ValueQuery>; + + // The block where all locked dots will unlock. + #[pallet::storage] + pub(super) type UnlockBlock<T: Config> = StorageValue<_, BlockNumberFor<T>, ValueQuery>; + + #[pallet::hooks] + impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {} + + #[pallet::call] + impl<T: Config> Pallet<T> { + /// Create a new account. Proof of existence through a valid signed message. + /// + /// We check that the account does not exist at this stage. + /// + /// Origin must match the `ValidityOrigin`. + #[pallet::call_index(0)] + #[pallet::weight(Weight::from_parts(200_000_000, 0) + T::DbWeight::get().reads_writes(4, 1))] + pub fn create_account( + origin: OriginFor<T>, + who: T::AccountId, + signature: Vec<u8>, + ) -> DispatchResult { + T::ValidityOrigin::ensure_origin(origin)?; + // Account is already being tracked by the pallet. + ensure!(!Accounts::<T>::contains_key(&who), Error::<T>::ExistingAccount); + // Account should not have a vesting schedule. + ensure!( + T::VestingSchedule::vesting_balance(&who).is_none(), + Error::<T>::VestingScheduleExists + ); + + // Verify the signature provided is valid for the statement. + Self::verify_signature(&who, &signature)?; + + // Create a new pending account. + let status = AccountStatus { + validity: AccountValidity::Initiated, + signature, + free_balance: Zero::zero(), + locked_balance: Zero::zero(), + vat: Permill::zero(), + }; + Accounts::<T>::insert(&who, status); + Self::deposit_event(Event::<T>::AccountCreated { who }); + Ok(()) + } + + /// Update the validity status of an existing account. If set to completed, the account + /// will no longer be able to continue through the crowdfund process. + /// + /// We check that the account exists at this stage, but has not completed the process. + /// + /// Origin must match the `ValidityOrigin`. + #[pallet::call_index(1)] + #[pallet::weight(T::DbWeight::get().reads_writes(1, 1))] + pub fn update_validity_status( + origin: OriginFor<T>, + who: T::AccountId, + validity: AccountValidity, + ) -> DispatchResult { + T::ValidityOrigin::ensure_origin(origin)?; + ensure!(Accounts::<T>::contains_key(&who), Error::<T>::InvalidAccount); + Accounts::<T>::try_mutate( + &who, + |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult { + ensure!( + status.validity != AccountValidity::Completed, + Error::<T>::AlreadyCompleted + ); + status.validity = validity; + Ok(()) + }, + )?; + Self::deposit_event(Event::<T>::ValidityUpdated { who, validity }); + Ok(()) + } + + /// Update the balance of a valid account. + /// + /// We check that the account is valid for a balance transfer at this point. + /// + /// Origin must match the `ValidityOrigin`. + #[pallet::call_index(2)] + #[pallet::weight(T::DbWeight::get().reads_writes(2, 1))] + pub fn update_balance( + origin: OriginFor<T>, + who: T::AccountId, + free_balance: BalanceOf<T>, + locked_balance: BalanceOf<T>, + vat: Permill, + ) -> DispatchResult { + T::ValidityOrigin::ensure_origin(origin)?; + + Accounts::<T>::try_mutate( + &who, + |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult { + // Account has a valid status (not Invalid, Pending, or Completed)... + ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount); + + free_balance.checked_add(&locked_balance).ok_or(Error::<T>::Overflow)?; + status.free_balance = free_balance; + status.locked_balance = locked_balance; + status.vat = vat; + Ok(()) + }, + )?; + Self::deposit_event(Event::<T>::BalanceUpdated { + who, + free: free_balance, + locked: locked_balance, + }); + Ok(()) + } + + /// Pay the user and complete the purchase process. + /// + /// We reverify all assumptions about the state of an account, and complete the process. + /// + /// Origin must match the configured `PaymentAccount` (if it is not configured then this + /// will always fail with `BadOrigin`). + #[pallet::call_index(3)] + #[pallet::weight(T::DbWeight::get().reads_writes(4, 2))] + pub fn payout(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult { + // Payments must be made directly by the `PaymentAccount`. + let payment_account = ensure_signed(origin)?; + let test_against = PaymentAccount::<T>::get().ok_or(DispatchError::BadOrigin)?; + ensure!(payment_account == test_against, DispatchError::BadOrigin); + + // Account should not have a vesting schedule. + ensure!( + T::VestingSchedule::vesting_balance(&who).is_none(), + Error::<T>::VestingScheduleExists + ); + + Accounts::<T>::try_mutate( + &who, + |status: &mut AccountStatus<BalanceOf<T>>| -> DispatchResult { + // Account has a valid status (not Invalid, Pending, or Completed)... + ensure!(status.validity.is_valid(), Error::<T>::InvalidAccount); + + // Transfer funds from the payment account into the purchasing user. + let total_balance = status + .free_balance + .checked_add(&status.locked_balance) + .ok_or(Error::<T>::Overflow)?; + T::Currency::transfer( + &payment_account, + &who, + total_balance, + ExistenceRequirement::AllowDeath, + )?; + + if !status.locked_balance.is_zero() { + let unlock_block = UnlockBlock::<T>::get(); + // We allow some configurable portion of the purchased locked DOTs to be + // unlocked for basic usage. + let unlocked = (T::UnlockedProportion::get() * status.locked_balance) + .min(T::MaxUnlocked::get()); + let locked = status.locked_balance.saturating_sub(unlocked); + // We checked that this account has no existing vesting schedule. So this + // function should never fail, however if it does, not much we can do about + // it at this point. + let _ = T::VestingSchedule::add_vesting_schedule( + // Apply vesting schedule to this user + &who, + // For this much amount + locked, + // Unlocking the full amount after one block + locked, + // When everything unlocks + unlock_block, + ); + } + + // Setting the user account to `Completed` ends the purchase process for this + // user. + status.validity = AccountValidity::Completed; + Self::deposit_event(Event::<T>::PaymentComplete { + who: who.clone(), + free: status.free_balance, + locked: status.locked_balance, + }); + Ok(()) + }, + )?; + Ok(()) + } + + /* Configuration Operations */ + + /// Set the account that will be used to payout users in the DOT purchase process. + /// + /// Origin must match the `ConfigurationOrigin` + #[pallet::call_index(4)] + #[pallet::weight(T::DbWeight::get().writes(1))] + pub fn set_payment_account(origin: OriginFor<T>, who: T::AccountId) -> DispatchResult { + T::ConfigurationOrigin::ensure_origin(origin)?; + // Possibly this is worse than having the caller account be the payment account? + PaymentAccount::<T>::put(who.clone()); + Self::deposit_event(Event::<T>::PaymentAccountSet { who }); + Ok(()) + } + + /// Set the statement that must be signed for a user to participate on the DOT sale. + /// + /// Origin must match the `ConfigurationOrigin` + #[pallet::call_index(5)] + #[pallet::weight(T::DbWeight::get().writes(1))] + pub fn set_statement(origin: OriginFor<T>, statement: Vec<u8>) -> DispatchResult { + T::ConfigurationOrigin::ensure_origin(origin)?; + ensure!( + (statement.len() as u32) < T::MaxStatementLength::get(), + Error::<T>::InvalidStatement + ); + // Possibly this is worse than having the caller account be the payment account? + Statement::<T>::set(statement); + Self::deposit_event(Event::<T>::StatementUpdated); + Ok(()) + } + + /// Set the block where locked DOTs will become unlocked. + /// + /// Origin must match the `ConfigurationOrigin` + #[pallet::call_index(6)] + #[pallet::weight(T::DbWeight::get().writes(1))] + pub fn set_unlock_block( + origin: OriginFor<T>, + unlock_block: BlockNumberFor<T>, + ) -> DispatchResult { + T::ConfigurationOrigin::ensure_origin(origin)?; + ensure!( + unlock_block > frame_system::Pallet::<T>::block_number(), + Error::<T>::InvalidUnlockBlock + ); + // Possibly this is worse than having the caller account be the payment account? + UnlockBlock::<T>::set(unlock_block); + Self::deposit_event(Event::<T>::UnlockBlockUpdated { block_number: unlock_block }); + Ok(()) + } + } +} + +impl<T: Config> Pallet<T> { + fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> { + // sr25519 always expects a 64 byte signature. + let signature: AnySignature = sr25519::Signature::try_from(signature) + .map_err(|_| Error::<T>::InvalidSignature)? + .into(); + + // In Polkadot, the AccountId is always the same as the 32 byte public key. + let account_bytes: [u8; 32] = account_to_bytes(who)?; + let public_key = sr25519::Public::from_raw(account_bytes); + + let message = Statement::<T>::get(); + + // Check if everything is good or not. + match signature.verify(message.as_slice(), &public_key) { + true => Ok(()), + false => Err(Error::<T>::InvalidSignature)?, + } + } +} + +// This function converts a 32 byte AccountId to its byte-array equivalent form. +fn account_to_bytes<AccountId>(account: &AccountId) -> Result<[u8; 32], DispatchError> +where + AccountId: Encode, +{ + let account_vec = account.encode(); + ensure!(account_vec.len() == 32, "AccountId must be 32 bytes."); + let mut bytes = [0u8; 32]; + bytes.copy_from_slice(&account_vec); + Ok(bytes) +} + +/// WARNING: Executing this function will clear all storage used by this pallet. +/// Be sure this is what you want... +pub fn remove_pallet<T>() -> frame_support::weights::Weight +where + T: frame_system::Config, +{ + #[allow(deprecated)] + use frame_support::migration::remove_storage_prefix; + #[allow(deprecated)] + remove_storage_prefix(b"Purchase", b"Accounts", b""); + #[allow(deprecated)] + remove_storage_prefix(b"Purchase", b"PaymentAccount", b""); + #[allow(deprecated)] + remove_storage_prefix(b"Purchase", b"Statement", b""); + #[allow(deprecated)] + remove_storage_prefix(b"Purchase", b"UnlockBlock", b""); + + <T as frame_system::Config>::BlockWeights::get().max_block +} + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; diff --git a/polkadot/runtime/common/src/purchase/tests.rs b/polkadot/runtime/common/src/purchase/tests.rs new file mode 100644 index 00000000000..8cf2a124d24 --- /dev/null +++ b/polkadot/runtime/common/src/purchase/tests.rs @@ -0,0 +1,547 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// 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/>. + +//! Tests for the purchase pallet. + +#[cfg(test)] +use super::*; + +use sp_core::crypto::AccountId32; +// 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 required. +use frame_support::{assert_noop, assert_ok, traits::Currency}; +use sp_runtime::{traits::Dispatchable, ArithmeticError, DispatchError::BadOrigin}; + +use crate::purchase::mock::*; + +#[test] +fn set_statement_works_and_handles_basic_errors() { + new_test_ext().execute_with(|| { + let statement = b"Test Set Statement".to_vec(); + // Invalid origin + assert_noop!( + Purchase::set_statement(RuntimeOrigin::signed(alice()), statement.clone()), + BadOrigin, + ); + // Too Long + let long_statement = [0u8; 10_000].to_vec(); + assert_noop!( + Purchase::set_statement(RuntimeOrigin::signed(configuration_origin()), long_statement), + Error::<Test>::InvalidStatement, + ); + // Just right... + assert_ok!(Purchase::set_statement( + RuntimeOrigin::signed(configuration_origin()), + statement.clone() + )); + assert_eq!(Statement::<Test>::get(), statement); + }); +} + +#[test] +fn set_unlock_block_works_and_handles_basic_errors() { + new_test_ext().execute_with(|| { + let unlock_block = 69; + // Invalid origin + assert_noop!( + Purchase::set_unlock_block(RuntimeOrigin::signed(alice()), unlock_block), + BadOrigin, + ); + // Block Number in Past + let bad_unlock_block = 50; + System::set_block_number(bad_unlock_block); + assert_noop!( + Purchase::set_unlock_block( + RuntimeOrigin::signed(configuration_origin()), + bad_unlock_block + ), + Error::<Test>::InvalidUnlockBlock, + ); + // Just right... + assert_ok!(Purchase::set_unlock_block( + RuntimeOrigin::signed(configuration_origin()), + unlock_block + )); + assert_eq!(UnlockBlock::<Test>::get(), unlock_block); + }); +} + +#[test] +fn set_payment_account_works_and_handles_basic_errors() { + new_test_ext().execute_with(|| { + let payment_account: AccountId32 = [69u8; 32].into(); + // Invalid Origin + assert_noop!( + Purchase::set_payment_account(RuntimeOrigin::signed(alice()), payment_account.clone()), + BadOrigin, + ); + // Just right... + assert_ok!(Purchase::set_payment_account( + RuntimeOrigin::signed(configuration_origin()), + payment_account.clone() + )); + assert_eq!(PaymentAccount::<Test>::get(), Some(payment_account)); + }); +} + +#[test] +fn signature_verification_works() { + new_test_ext().execute_with(|| { + assert_ok!(Purchase::verify_signature(&alice(), &alice_signature())); + assert_ok!(Purchase::verify_signature(&alice_ed25519(), &alice_signature_ed25519())); + assert_ok!(Purchase::verify_signature(&bob(), &bob_signature())); + + // Mixing and matching fails + assert_noop!( + Purchase::verify_signature(&alice(), &bob_signature()), + Error::<Test>::InvalidSignature + ); + assert_noop!( + Purchase::verify_signature(&bob(), &alice_signature()), + Error::<Test>::InvalidSignature + ); + }); +} + +#[test] +fn account_creation_works() { + new_test_ext().execute_with(|| { + assert!(!Accounts::<Test>::contains_key(alice())); + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec(), + )); + assert_eq!( + Accounts::<Test>::get(alice()), + AccountStatus { + validity: AccountValidity::Initiated, + free_balance: Zero::zero(), + locked_balance: Zero::zero(), + signature: alice_signature().to_vec(), + vat: Permill::zero(), + } + ); + }); +} + +#[test] +fn account_creation_handles_basic_errors() { + new_test_ext().execute_with(|| { + // Wrong Origin + assert_noop!( + Purchase::create_account( + RuntimeOrigin::signed(alice()), + alice(), + alice_signature().to_vec() + ), + BadOrigin, + ); + + // Wrong Account/Signature + assert_noop!( + Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + bob_signature().to_vec() + ), + Error::<Test>::InvalidSignature, + ); + + // Account with vesting + Balances::make_free_balance_be(&alice(), 100); + assert_ok!(<Test as Config>::VestingSchedule::add_vesting_schedule(&alice(), 100, 1, 50)); + assert_noop!( + Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec() + ), + Error::<Test>::VestingScheduleExists, + ); + + // Duplicate Purchasing Account + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + bob(), + bob_signature().to_vec() + )); + assert_noop!( + Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + bob(), + bob_signature().to_vec() + ), + Error::<Test>::ExistingAccount, + ); + }); +} + +#[test] +fn update_validity_status_works() { + new_test_ext().execute_with(|| { + // Alice account is created. + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec(), + )); + // She submits KYC, and we update the status to `Pending`. + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::Pending, + )); + // KYC comes back negative, so we mark the account invalid. + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::Invalid, + )); + assert_eq!( + Accounts::<Test>::get(alice()), + AccountStatus { + validity: AccountValidity::Invalid, + free_balance: Zero::zero(), + locked_balance: Zero::zero(), + signature: alice_signature().to_vec(), + vat: Permill::zero(), + } + ); + // She fixes it, we mark her account valid. + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::ValidLow, + )); + assert_eq!( + Accounts::<Test>::get(alice()), + AccountStatus { + validity: AccountValidity::ValidLow, + free_balance: Zero::zero(), + locked_balance: Zero::zero(), + signature: alice_signature().to_vec(), + vat: Permill::zero(), + } + ); + }); +} + +#[test] +fn update_validity_status_handles_basic_errors() { + new_test_ext().execute_with(|| { + // Wrong Origin + assert_noop!( + Purchase::update_validity_status( + RuntimeOrigin::signed(alice()), + alice(), + AccountValidity::Pending, + ), + BadOrigin + ); + // Inactive Account + assert_noop!( + Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::Pending, + ), + Error::<Test>::InvalidAccount + ); + // Already Completed + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec(), + )); + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::Completed, + )); + assert_noop!( + Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::Pending, + ), + Error::<Test>::AlreadyCompleted + ); + }); +} + +#[test] +fn update_balance_works() { + new_test_ext().execute_with(|| { + // Alice account is created + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec() + )); + // And approved for basic contribution + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::ValidLow, + )); + // We set a balance on the user based on the payment they made. 50 locked, 50 free. + assert_ok!(Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + alice(), + 50, + 50, + Permill::from_rational(77u32, 1000u32), + )); + assert_eq!( + Accounts::<Test>::get(alice()), + AccountStatus { + validity: AccountValidity::ValidLow, + free_balance: 50, + locked_balance: 50, + signature: alice_signature().to_vec(), + vat: Permill::from_parts(77000), + } + ); + // We can update the balance based on new information. + assert_ok!(Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + alice(), + 25, + 50, + Permill::zero(), + )); + assert_eq!( + Accounts::<Test>::get(alice()), + AccountStatus { + validity: AccountValidity::ValidLow, + free_balance: 25, + locked_balance: 50, + signature: alice_signature().to_vec(), + vat: Permill::zero(), + } + ); + }); +} + +#[test] +fn update_balance_handles_basic_errors() { + new_test_ext().execute_with(|| { + // Wrong Origin + assert_noop!( + Purchase::update_balance( + RuntimeOrigin::signed(alice()), + alice(), + 50, + 50, + Permill::zero(), + ), + BadOrigin + ); + // Inactive Account + assert_noop!( + Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + alice(), + 50, + 50, + Permill::zero(), + ), + Error::<Test>::InvalidAccount + ); + // Overflow + assert_noop!( + Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + alice(), + u64::MAX, + u64::MAX, + Permill::zero(), + ), + Error::<Test>::InvalidAccount + ); + }); +} + +#[test] +fn payout_works() { + new_test_ext().execute_with(|| { + // Alice and Bob accounts are created + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec() + )); + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + bob(), + bob_signature().to_vec() + )); + // Alice is approved for basic contribution + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::ValidLow, + )); + // Bob is approved for high contribution + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + bob(), + AccountValidity::ValidHigh, + )); + // We set a balance on the users based on the payment they made. 50 locked, 50 free. + assert_ok!(Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + alice(), + 50, + 50, + Permill::zero(), + )); + assert_ok!(Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + bob(), + 100, + 150, + Permill::zero(), + )); + // Now we call payout for Alice and Bob. + assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),)); + assert_ok!(Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),)); + // Payment is made. + assert_eq!(<Test as Config>::Currency::free_balance(&payment_account()), 99_650); + assert_eq!(<Test as Config>::Currency::free_balance(&alice()), 100); + // 10% of the 50 units is unlocked automatically for Alice + assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45)); + assert_eq!(<Test as Config>::Currency::free_balance(&bob()), 250); + // A max of 10 units is unlocked automatically for Bob + assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140)); + // Status is completed. + assert_eq!( + Accounts::<Test>::get(alice()), + AccountStatus { + validity: AccountValidity::Completed, + free_balance: 50, + locked_balance: 50, + signature: alice_signature().to_vec(), + vat: Permill::zero(), + } + ); + assert_eq!( + Accounts::<Test>::get(bob()), + AccountStatus { + validity: AccountValidity::Completed, + free_balance: 100, + locked_balance: 150, + signature: bob_signature().to_vec(), + vat: Permill::zero(), + } + ); + // Vesting lock is removed in whole on block 101 (100 blocks after block 1) + System::set_block_number(100); + let vest_call = RuntimeCall::Vesting(pallet_vesting::Call::<Test>::vest {}); + assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice()))); + assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob()))); + assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), Some(45)); + assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), Some(140)); + System::set_block_number(101); + assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice()))); + assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob()))); + assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&alice()), None); + assert_eq!(<Test as Config>::VestingSchedule::vesting_balance(&bob()), None); + }); +} + +#[test] +fn payout_handles_basic_errors() { + new_test_ext().execute_with(|| { + // Wrong Origin + assert_noop!(Purchase::payout(RuntimeOrigin::signed(alice()), alice(),), BadOrigin); + // Account with Existing Vesting Schedule + Balances::make_free_balance_be(&bob(), 100); + assert_ok!(<Test as Config>::VestingSchedule::add_vesting_schedule(&bob(), 100, 1, 50,)); + assert_noop!( + Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),), + Error::<Test>::VestingScheduleExists + ); + // Invalid Account (never created) + assert_noop!( + Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),), + Error::<Test>::InvalidAccount + ); + // Invalid Account (created, but not valid) + assert_ok!(Purchase::create_account( + RuntimeOrigin::signed(validity_origin()), + alice(), + alice_signature().to_vec() + )); + assert_noop!( + Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),), + Error::<Test>::InvalidAccount + ); + // Not enough funds in payment account + assert_ok!(Purchase::update_validity_status( + RuntimeOrigin::signed(validity_origin()), + alice(), + AccountValidity::ValidHigh, + )); + assert_ok!(Purchase::update_balance( + RuntimeOrigin::signed(validity_origin()), + alice(), + 100_000, + 100_000, + Permill::zero(), + )); + assert_noop!( + Purchase::payout(RuntimeOrigin::signed(payment_account()), alice()), + ArithmeticError::Underflow + ); + }); +} + +#[test] +fn remove_pallet_works() { + new_test_ext().execute_with(|| { + let account_status = AccountStatus { + validity: AccountValidity::Completed, + free_balance: 1234, + locked_balance: 4321, + signature: b"my signature".to_vec(), + vat: Permill::from_percent(50), + }; + + // Add some storage. + Accounts::<Test>::insert(alice(), account_status.clone()); + Accounts::<Test>::insert(bob(), account_status); + PaymentAccount::<Test>::put(alice()); + Statement::<Test>::put(b"hello, world!".to_vec()); + UnlockBlock::<Test>::put(4); + + // Verify storage exists. + assert_eq!(Accounts::<Test>::iter().count(), 2); + assert!(PaymentAccount::<Test>::exists()); + assert!(Statement::<Test>::exists()); + assert!(UnlockBlock::<Test>::exists()); + + // Remove storage. + remove_pallet::<Test>(); + + // Verify storage is gone. + assert_eq!(Accounts::<Test>::iter().count(), 0); + assert!(!PaymentAccount::<Test>::exists()); + assert!(!Statement::<Test>::exists()); + assert!(!UnlockBlock::<Test>::exists()); + }); +} -- GitLab