diff --git a/polkadot/runtime/common/src/purchase.rs b/polkadot/runtime/common/src/purchase.rs
deleted file mode 100644
index cec92540654c..000000000000
--- 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 .
-
-//! 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 =
- <::Currency as Currency<::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 {
- /// 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,
- /// 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(_);
-
- #[pallet::config]
- pub trait Config: frame_system::Config {
- /// The overarching event type.
- type RuntimeEvent: From> + IsType<::RuntimeEvent>;
-
- /// Balances Pallet
- type Currency: Currency;
-
- /// Vesting Pallet
- type VestingSchedule: VestingSchedule<
- Self::AccountId,
- Moment = BlockNumberFor,
- Currency = Self::Currency,
- >;
-
- /// The origin allowed to set account status.
- type ValidityOrigin: EnsureOrigin;
-
- /// The origin allowed to make configurations to the pallet.
- type ConfigurationOrigin: EnsureOrigin;
-
- /// The maximum statement length for the statement users to sign when creating an account.
- #[pallet::constant]
- type MaxStatementLength: Get;
-
- /// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
- #[pallet::constant]
- type UnlockedProportion: Get;
-
- /// The maximum amount of locked DOTs that we will unlock.
- #[pallet::constant]
- type MaxUnlocked: Get>;
- }
-
- #[pallet::event]
- #[pallet::generate_deposit(pub(super) fn deposit_event)]
- pub enum Event {
- /// 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, locked: BalanceOf },
- /// A payout was made to a purchaser.
- PaymentComplete { who: T::AccountId, free: BalanceOf, locked: BalanceOf },
- /// 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 },
- }
-
- #[pallet::error]
- pub enum Error {
- /// 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 =
- StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus>, ValueQuery>;
-
- // The account that will be used to payout participants of the DOT purchase process.
- #[pallet::storage]
- pub(super) type PaymentAccount = StorageValue<_, T::AccountId, OptionQuery>;
-
- // The statement purchasers will need to sign to participate.
- #[pallet::storage]
- pub(super) type Statement = StorageValue<_, Vec, ValueQuery>;
-
- // The block where all locked dots will unlock.
- #[pallet::storage]
- pub(super) type UnlockBlock = StorageValue<_, BlockNumberFor, ValueQuery>;
-
- #[pallet::hooks]
- impl Hooks> for Pallet {}
-
- #[pallet::call]
- impl Pallet {
- /// 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,
- who: T::AccountId,
- signature: Vec,
- ) -> DispatchResult {
- T::ValidityOrigin::ensure_origin(origin)?;
- // Account is already being tracked by the pallet.
- ensure!(!Accounts::::contains_key(&who), Error::::ExistingAccount);
- // Account should not have a vesting schedule.
- ensure!(
- T::VestingSchedule::vesting_balance(&who).is_none(),
- Error::::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::::insert(&who, status);
- Self::deposit_event(Event::::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,
- who: T::AccountId,
- validity: AccountValidity,
- ) -> DispatchResult {
- T::ValidityOrigin::ensure_origin(origin)?;
- ensure!(Accounts::::contains_key(&who), Error::::InvalidAccount);
- Accounts::::try_mutate(
- &who,
- |status: &mut AccountStatus>| -> DispatchResult {
- ensure!(
- status.validity != AccountValidity::Completed,
- Error::::AlreadyCompleted
- );
- status.validity = validity;
- Ok(())
- },
- )?;
- Self::deposit_event(Event::::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,
- who: T::AccountId,
- free_balance: BalanceOf,
- locked_balance: BalanceOf,
- vat: Permill,
- ) -> DispatchResult {
- T::ValidityOrigin::ensure_origin(origin)?;
-
- Accounts::::try_mutate(
- &who,
- |status: &mut AccountStatus>| -> DispatchResult {
- // Account has a valid status (not Invalid, Pending, or Completed)...
- ensure!(status.validity.is_valid(), Error::::InvalidAccount);
-
- free_balance.checked_add(&locked_balance).ok_or(Error::::Overflow)?;
- status.free_balance = free_balance;
- status.locked_balance = locked_balance;
- status.vat = vat;
- Ok(())
- },
- )?;
- Self::deposit_event(Event::::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, who: T::AccountId) -> DispatchResult {
- // Payments must be made directly by the `PaymentAccount`.
- let payment_account = ensure_signed(origin)?;
- let test_against = PaymentAccount::::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::::VestingScheduleExists
- );
-
- Accounts::::try_mutate(
- &who,
- |status: &mut AccountStatus>| -> DispatchResult {
- // Account has a valid status (not Invalid, Pending, or Completed)...
- ensure!(status.validity.is_valid(), Error::::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::::Overflow)?;
- T::Currency::transfer(
- &payment_account,
- &who,
- total_balance,
- ExistenceRequirement::AllowDeath,
- )?;
-
- if !status.locked_balance.is_zero() {
- let unlock_block = UnlockBlock::::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::::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, who: T::AccountId) -> DispatchResult {
- T::ConfigurationOrigin::ensure_origin(origin)?;
- // Possibly this is worse than having the caller account be the payment account?
- PaymentAccount::::put(who.clone());
- Self::deposit_event(Event::::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, statement: Vec) -> DispatchResult {
- T::ConfigurationOrigin::ensure_origin(origin)?;
- ensure!(
- (statement.len() as u32) < T::MaxStatementLength::get(),
- Error::::InvalidStatement
- );
- // Possibly this is worse than having the caller account be the payment account?
- Statement::::set(statement);
- Self::deposit_event(Event::::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,
- unlock_block: BlockNumberFor,
- ) -> DispatchResult {
- T::ConfigurationOrigin::ensure_origin(origin)?;
- ensure!(
- unlock_block > frame_system::Pallet::::block_number(),
- Error::::InvalidUnlockBlock
- );
- // Possibly this is worse than having the caller account be the payment account?
- UnlockBlock::::set(unlock_block);
- Self::deposit_event(Event::::UnlockBlockUpdated { block_number: unlock_block });
- Ok(())
- }
- }
-}
-
-impl Pallet {
- 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::::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::::get();
-
- // Check if everything is good or not.
- match signature.verify(message.as_slice(), &public_key) {
- true => Ok(()),
- false => Err(Error::::InvalidSignature)?,
- }
- }
-}
-
-// This function converts a 32 byte AccountId to its byte-array equivalent form.
-fn account_to_bytes(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() -> 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"");
-
- ::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;
-
- 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;
- type Block = Block;
- type RuntimeEvent = RuntimeEvent;
- type Version = ();
- type PalletInfo = PalletInfo;
- type AccountData = pallet_balances::AccountData;
- 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;
- type ConfigurationOrigin = frame_system::EnsureSignedBy;
- 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::::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::::InvalidStatement,
- );
- // Just right...
- assert_ok!(Purchase::set_statement(
- RuntimeOrigin::signed(configuration_origin()),
- statement.clone()
- ));
- assert_eq!(Statement::::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::::InvalidUnlockBlock,
- );
- // Just right...
- assert_ok!(Purchase::set_unlock_block(
- RuntimeOrigin::signed(configuration_origin()),
- unlock_block
- ));
- assert_eq!(UnlockBlock::::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::::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::::InvalidSignature
- );
- assert_noop!(
- Purchase::verify_signature(&bob(), &alice_signature()),
- Error::::InvalidSignature
- );
- });
- }
-
- #[test]
- fn account_creation_works() {
- new_test_ext().execute_with(|| {
- assert!(!Accounts::::contains_key(alice()));
- assert_ok!(Purchase::create_account(
- RuntimeOrigin::signed(validity_origin()),
- alice(),
- alice_signature().to_vec(),
- ));
- assert_eq!(
- Accounts::::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::::InvalidSignature,
- );
-
- // Account with vesting
- Balances::make_free_balance_be(&alice(), 100);
- assert_ok!(::VestingSchedule::add_vesting_schedule(
- &alice(),
- 100,
- 1,
- 50
- ));
- assert_noop!(
- Purchase::create_account(
- RuntimeOrigin::signed(validity_origin()),
- alice(),
- alice_signature().to_vec()
- ),
- Error::::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::::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::::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::::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::::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::::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::::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::::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::::InvalidAccount
- );
- // Overflow
- assert_noop!(
- Purchase::update_balance(
- RuntimeOrigin::signed(validity_origin()),
- alice(),
- u64::MAX,
- u64::MAX,
- Permill::zero(),
- ),
- Error::::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!(::Currency::free_balance(&payment_account()), 99_650);
- assert_eq!(::Currency::free_balance(&alice()), 100);
- // 10% of the 50 units is unlocked automatically for Alice
- assert_eq!(::VestingSchedule::vesting_balance(&alice()), Some(45));
- assert_eq!(::Currency::free_balance(&bob()), 250);
- // A max of 10 units is unlocked automatically for Bob
- assert_eq!(::VestingSchedule::vesting_balance(&bob()), Some(140));
- // Status is completed.
- assert_eq!(
- Accounts::::get(alice()),
- AccountStatus {
- validity: AccountValidity::Completed,
- free_balance: 50,
- locked_balance: 50,
- signature: alice_signature().to_vec(),
- vat: Permill::zero(),
- }
- );
- assert_eq!(
- Accounts::::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::::vest {});
- assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(alice())));
- assert_ok!(vest_call.clone().dispatch(RuntimeOrigin::signed(bob())));
- assert_eq!(::VestingSchedule::vesting_balance(&alice()), Some(45));
- assert_eq!(::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!(::VestingSchedule::vesting_balance(&alice()), None);
- assert_eq!(::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!(
- ::VestingSchedule::add_vesting_schedule(&bob(), 100, 1, 50,)
- );
- assert_noop!(
- Purchase::payout(RuntimeOrigin::signed(payment_account()), bob(),),
- Error::::VestingScheduleExists
- );
- // Invalid Account (never created)
- assert_noop!(
- Purchase::payout(RuntimeOrigin::signed(payment_account()), alice(),),
- Error::::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::::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::::insert(alice(), account_status.clone());
- Accounts::::insert(bob(), account_status);
- PaymentAccount::::put(alice());
- Statement::::put(b"hello, world!".to_vec());
- UnlockBlock::::put(4);
-
- // Verify storage exists.
- assert_eq!(Accounts::::iter().count(), 2);
- assert!(PaymentAccount::::exists());
- assert!(Statement::::exists());
- assert!(UnlockBlock::::exists());
-
- // Remove storage.
- remove_pallet::();
-
- // Verify storage is gone.
- assert_eq!(Accounts::::iter().count(), 0);
- assert!(!PaymentAccount::::exists());
- assert!(!Statement::::exists());
- assert!(!UnlockBlock::::exists());
- });
- }
-}
diff --git a/polkadot/runtime/common/src/purchase/mock.rs b/polkadot/runtime/common/src/purchase/mock.rs
new file mode 100644
index 000000000000..ec8599f3b792
--- /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 .
+
+//! 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;
+
+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;
+ type Block = Block;
+ type RuntimeEvent = RuntimeEvent;
+ type Version = ();
+ type PalletInfo = PalletInfo;
+ type AccountData = pallet_balances::AccountData;
+ 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;
+ type ConfigurationOrigin = frame_system::EnsureSignedBy;
+ 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::::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 000000000000..71dc5b579670
--- /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 .
+
+//! 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 =
+ <::Currency as Currency<::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 {
+ /// 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,
+ /// 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(_);
+
+ #[pallet::config]
+ pub trait Config: frame_system::Config {
+ /// The overarching event type.
+ type RuntimeEvent: From> + IsType<::RuntimeEvent>;
+
+ /// Balances Pallet
+ type Currency: Currency;
+
+ /// Vesting Pallet
+ type VestingSchedule: VestingSchedule<
+ Self::AccountId,
+ Moment = BlockNumberFor,
+ Currency = Self::Currency,
+ >;
+
+ /// The origin allowed to set account status.
+ type ValidityOrigin: EnsureOrigin;
+
+ /// The origin allowed to make configurations to the pallet.
+ type ConfigurationOrigin: EnsureOrigin;
+
+ /// The maximum statement length for the statement users to sign when creating an account.
+ #[pallet::constant]
+ type MaxStatementLength: Get;
+
+ /// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
+ #[pallet::constant]
+ type UnlockedProportion: Get;
+
+ /// The maximum amount of locked DOTs that we will unlock.
+ #[pallet::constant]
+ type MaxUnlocked: Get>;
+ }
+
+ #[pallet::event]
+ #[pallet::generate_deposit(pub(super) fn deposit_event)]
+ pub enum Event {
+ /// 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, locked: BalanceOf },
+ /// A payout was made to a purchaser.
+ PaymentComplete { who: T::AccountId, free: BalanceOf, locked: BalanceOf },
+ /// 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 },
+ }
+
+ #[pallet::error]
+ pub enum Error {
+ /// 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 =
+ StorageMap<_, Blake2_128Concat, T::AccountId, AccountStatus>, ValueQuery>;
+
+ // The account that will be used to payout participants of the DOT purchase process.
+ #[pallet::storage]
+ pub(super) type PaymentAccount = StorageValue<_, T::AccountId, OptionQuery>;
+
+ // The statement purchasers will need to sign to participate.
+ #[pallet::storage]
+ pub(super) type Statement = StorageValue<_, Vec, ValueQuery>;
+
+ // The block where all locked dots will unlock.
+ #[pallet::storage]
+ pub(super) type UnlockBlock = StorageValue<_, BlockNumberFor, ValueQuery>;
+
+ #[pallet::hooks]
+ impl Hooks> for Pallet {}
+
+ #[pallet::call]
+ impl Pallet {
+ /// 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,
+ who: T::AccountId,
+ signature: Vec,
+ ) -> DispatchResult {
+ T::ValidityOrigin::ensure_origin(origin)?;
+ // Account is already being tracked by the pallet.
+ ensure!(!Accounts::::contains_key(&who), Error::::ExistingAccount);
+ // Account should not have a vesting schedule.
+ ensure!(
+ T::VestingSchedule::vesting_balance(&who).is_none(),
+ Error::::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::::insert(&who, status);
+ Self::deposit_event(Event::::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,
+ who: T::AccountId,
+ validity: AccountValidity,
+ ) -> DispatchResult {
+ T::ValidityOrigin::ensure_origin(origin)?;
+ ensure!(Accounts::::contains_key(&who), Error::::InvalidAccount);
+ Accounts::::try_mutate(
+ &who,
+ |status: &mut AccountStatus>| -> DispatchResult {
+ ensure!(
+ status.validity != AccountValidity::Completed,
+ Error::::AlreadyCompleted
+ );
+ status.validity = validity;
+ Ok(())
+ },
+ )?;
+ Self::deposit_event(Event::::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,
+ who: T::AccountId,
+ free_balance: BalanceOf,
+ locked_balance: BalanceOf,
+ vat: Permill,
+ ) -> DispatchResult {
+ T::ValidityOrigin::ensure_origin(origin)?;
+
+ Accounts::::try_mutate(
+ &who,
+ |status: &mut AccountStatus>| -> DispatchResult {
+ // Account has a valid status (not Invalid, Pending, or Completed)...
+ ensure!(status.validity.is_valid(), Error::::InvalidAccount);
+
+ free_balance.checked_add(&locked_balance).ok_or(Error::::Overflow)?;
+ status.free_balance = free_balance;
+ status.locked_balance = locked_balance;
+ status.vat = vat;
+ Ok(())
+ },
+ )?;
+ Self::deposit_event(Event::::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, who: T::AccountId) -> DispatchResult {
+ // Payments must be made directly by the `PaymentAccount`.
+ let payment_account = ensure_signed(origin)?;
+ let test_against = PaymentAccount::::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::::VestingScheduleExists
+ );
+
+ Accounts::::try_mutate(
+ &who,
+ |status: &mut AccountStatus>| -> DispatchResult {
+ // Account has a valid status (not Invalid, Pending, or Completed)...
+ ensure!(status.validity.is_valid(), Error::::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::::Overflow)?;
+ T::Currency::transfer(
+ &payment_account,
+ &who,
+ total_balance,
+ ExistenceRequirement::AllowDeath,
+ )?;
+
+ if !status.locked_balance.is_zero() {
+ let unlock_block = UnlockBlock::::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::::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, who: T::AccountId) -> DispatchResult {
+ T::ConfigurationOrigin::ensure_origin(origin)?;
+ // Possibly this is worse than having the caller account be the payment account?
+ PaymentAccount::::put(who.clone());
+ Self::deposit_event(Event::::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, statement: Vec) -> DispatchResult {
+ T::ConfigurationOrigin::ensure_origin(origin)?;
+ ensure!(
+ (statement.len() as u32) < T::MaxStatementLength::get(),
+ Error::::InvalidStatement
+ );
+ // Possibly this is worse than having the caller account be the payment account?
+ Statement::::set(statement);
+ Self::deposit_event(Event::::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,
+ unlock_block: BlockNumberFor,
+ ) -> DispatchResult {
+ T::ConfigurationOrigin::ensure_origin(origin)?;
+ ensure!(
+ unlock_block > frame_system::Pallet::::block_number(),
+ Error::::InvalidUnlockBlock
+ );
+ // Possibly this is worse than having the caller account be the payment account?
+ UnlockBlock::::set(unlock_block);
+ Self::deposit_event(Event::::UnlockBlockUpdated { block_number: unlock_block });
+ Ok(())
+ }
+ }
+}
+
+impl Pallet {
+ 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::::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::::get();
+
+ // Check if everything is good or not.
+ match signature.verify(message.as_slice(), &public_key) {
+ true => Ok(()),
+ false => Err(Error::::InvalidSignature)?,
+ }
+ }
+}
+
+// This function converts a 32 byte AccountId to its byte-array equivalent form.
+fn account_to_bytes(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() -> 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"");
+
+ ::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 000000000000..8cf2a124d245
--- /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 .
+
+//! 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::::InvalidStatement,
+ );
+ // Just right...
+ assert_ok!(Purchase::set_statement(
+ RuntimeOrigin::signed(configuration_origin()),
+ statement.clone()
+ ));
+ assert_eq!(Statement::::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::::InvalidUnlockBlock,
+ );
+ // Just right...
+ assert_ok!(Purchase::set_unlock_block(
+ RuntimeOrigin::signed(configuration_origin()),
+ unlock_block
+ ));
+ assert_eq!(UnlockBlock::::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::::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::::InvalidSignature
+ );
+ assert_noop!(
+ Purchase::verify_signature(&bob(), &alice_signature()),
+ Error::::InvalidSignature
+ );
+ });
+}
+
+#[test]
+fn account_creation_works() {
+ new_test_ext().execute_with(|| {
+ assert!(!Accounts::::contains_key(alice()));
+ assert_ok!(Purchase::create_account(
+ RuntimeOrigin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec(),
+ ));
+ assert_eq!(
+ Accounts::::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::::InvalidSignature,
+ );
+
+ // Account with vesting
+ Balances::make_free_balance_be(&alice(), 100);
+ assert_ok!(::VestingSchedule::add_vesting_schedule(&alice(), 100, 1, 50));
+ assert_noop!(
+ Purchase::create_account(
+ RuntimeOrigin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec()
+ ),
+ Error::::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::::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::::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::::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::::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::::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::