diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs
index d32db38e05be..72a4ae3bfdd0 100644
--- a/runtime/common/src/lib.rs
+++ b/runtime/common/src/lib.rs
@@ -25,6 +25,7 @@ pub mod slot_range;
pub mod registrar;
pub mod slots;
pub mod crowdfund;
+pub mod purchase;
pub mod impls;
use primitives::v0::BlockNumber;
diff --git a/runtime/common/src/purchase.rs b/runtime/common/src/purchase.rs
new file mode 100644
index 000000000000..b1b61ebb439b
--- /dev/null
+++ b/runtime/common/src/purchase.rs
@@ -0,0 +1,1014 @@
+// Copyright 2017-2020 Parity Technologies (UK) Ltd.
+// This file is part of Polkadot.
+
+// Substrate 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.
+
+// Substrate 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 Substrate. If not, see .
+
+//! Module to process purchase of DOTs.
+
+use codec::{Encode, Decode};
+use sp_runtime::{Permill, RuntimeDebug, DispatchResult, DispatchError, AnySignature};
+use sp_runtime::traits::{Zero, CheckedAdd, Verify, Saturating};
+use frame_support::{decl_event, decl_storage, decl_module, decl_error, ensure};
+use frame_support::traits::{
+ EnsureOrigin, Currency, ExistenceRequirement, VestingSchedule, Get
+};
+use system::ensure_signed;
+use sp_core::sr25519;
+use sp_std::prelude::*;
+
+/// Configuration trait.
+pub trait Trait: system::Trait {
+ /// The overarching event type.
+ type Event: From> + Into<::Event>;
+ /// Balances Pallet
+ type Currency: Currency;
+ /// Vesting Pallet
+ type VestingSchedule: VestingSchedule;
+ /// 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.
+ type MaxStatementLength: Get;
+ /// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
+ type UnlockedProportion: Get;
+ /// The maximum amount of locked DOTs that we will unlock.
+ type MaxUnlocked: Get>;
+}
+
+type BalanceOf = <::Currency as Currency<::AccountId>>::Balance;
+
+/// The kind of a statement an account needs to make for a claim to be valid.
+#[derive(Encode, Decode, Clone, Copy, Eq, PartialEq, RuntimeDebug)]
+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)]
+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,
+}
+
+decl_event!(
+ pub enum Event where
+ AccountId = ::AccountId,
+ Balance = BalanceOf,
+ BlockNumber = ::BlockNumber,
+ {
+ /// A new account was created
+ AccountCreated(AccountId),
+ /// Someone's account validity was updated
+ ValidityUpdated(AccountId, AccountValidity),
+ /// Someone's purchase balance was updated. (Free, Locked)
+ BalanceUpdated(AccountId, Balance, Balance),
+ /// A payout was made to a purchaser.
+ PaymentComplete(AccountId, Balance, Balance),
+ /// A new payment account was set.
+ PaymentAccountSet(AccountId),
+ /// A new statement was set.
+ StatementUpdated,
+ /// A new statement was set.
+ UnlockBlockUpdated(BlockNumber),
+ }
+);
+
+decl_error! {
+ pub enum Error for Module {
+ /// 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,
+ }
+}
+
+decl_storage! {
+ trait Store for Module as Purchase {
+ // A map of all participants in the DOT purchase process.
+ Accounts: map hasher(blake2_128_concat) T::AccountId => AccountStatus>;
+ // The account that will be used to payout participants of the DOT purchase process.
+ PaymentAccount: T::AccountId;
+ // The statement purchasers will need to sign to participate.
+ Statement: Vec;
+ // The block where all locked dots will unlock.
+ UnlockBlock: T::BlockNumber;
+ }
+}
+
+decl_module! {
+ pub struct Module for enum Call where origin: T::Origin, system = system {
+ type Error = Error;
+
+ /// The maximum statement length for the statement users to sign when creating an account.
+ const MaxStatementLength: u32 = T::MaxStatementLength::get() as u32;
+ /// The amount of purchased locked DOTs that we will unlock for basic actions on the chain.
+ const UnlockedProportion: Permill = T::UnlockedProportion::get();
+ /// The maximum amount of locked DOTs that we will unlock.
+ const MaxUnlocked: BalanceOf = T::MaxUnlocked::get();
+
+ /// Deposit one of this module's events by using the default implementation.
+ fn deposit_event() = default;
+
+ /// 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`.
+ #[weight = 200_000_000 + T::DbWeight::get().reads_writes(4, 1)]
+ fn create_account(origin,
+ who: T::AccountId,
+ signature: Vec
+ ) {
+ 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(RawEvent::AccountCreated(who));
+ }
+
+ /// 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 tht the account exists at this stage, but has not completed the process.
+ ///
+ /// Origin must match the `ValidityOrigin`.
+ #[weight = T::DbWeight::get().reads_writes(1, 1)]
+ fn update_validity_status(origin,
+ who: T::AccountId,
+ validity: AccountValidity
+ ) {
+ 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(RawEvent::ValidityUpdated(who, validity));
+ }
+
+ /// Update the balance of a valid account.
+ ///
+ /// We check tht the account is valid for a balance transfer at this point.
+ ///
+ /// Origin must match the `ValidityOrigin`.
+ #[weight = T::DbWeight::get().reads_writes(2, 1)]
+ fn update_balance(origin,
+ who: T::AccountId,
+ free_balance: BalanceOf,
+ locked_balance: BalanceOf,
+ vat: Permill,
+ ) {
+ 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(RawEvent::BalanceUpdated(who, free_balance, locked_balance));
+ }
+
+ /// 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`.
+ #[weight = T::DbWeight::get().reads_writes(4, 2)]
+ fn payout(origin, who: T::AccountId) {
+ // Payments must be made directly by the `PaymentAccount`.
+ let payment_account = ensure_signed(origin)?;
+ ensure!(payment_account == PaymentAccount::::get(), 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(RawEvent::PaymentComplete(who.clone(), status.free_balance, status.locked_balance));
+ Ok(())
+ })?;
+ }
+
+ /* Configuration Operations */
+
+ /// Set the account that will be used to payout users in the DOT purchase process.
+ ///
+ /// Origin must match the `ConfigurationOrigin`
+ #[weight = T::DbWeight::get().writes(1)]
+ fn set_payment_account(origin, who: T::AccountId) {
+ T::ConfigurationOrigin::ensure_origin(origin)?;
+ // Possibly this is worse than having the caller account be the payment account?
+ PaymentAccount::::set(who.clone());
+ Self::deposit_event(RawEvent::PaymentAccountSet(who));
+ }
+
+ /// Set the statement that must be signed for a user to participate on the DOT sale.
+ ///
+ /// Origin must match the `ConfigurationOrigin`
+ #[weight = T::DbWeight::get().writes(1)]
+ fn set_statement(origin, statement: Vec) {
+ T::ConfigurationOrigin::ensure_origin(origin)?;
+ ensure!(statement.len() < T::MaxStatementLength::get(), Error::::InvalidStatement);
+ // Possibly this is worse than having the caller account be the payment account?
+ Statement::set(statement);
+ Self::deposit_event(RawEvent::StatementUpdated);
+ }
+
+ /// Set the block where locked DOTs will become unlocked.
+ ///
+ /// Origin must match the `ConfigurationOrigin`
+ #[weight = T::DbWeight::get().writes(1)]
+ fn set_unlock_block(origin, unlock_block: T::BlockNumber) {
+ T::ConfigurationOrigin::ensure_origin(origin)?;
+ ensure!(unlock_block > system::Module::::block_number(), Error::::InvalidUnlockBlock);
+ // Possibly this is worse than having the caller account be the payment account?
+ UnlockBlock::::set(unlock_block);
+ Self::deposit_event(RawEvent::UnlockBlockUpdated(unlock_block));
+ }
+ }
+}
+
+impl Module {
+ fn verify_signature(who: &T::AccountId, signature: &[u8]) -> Result<(), DispatchError> {
+ // sr25519 always expects a 64 byte signature.
+ ensure!(signature.len() == 64, Error::::InvalidSignature);
+ let signature: AnySignature = sr25519::Signature::from_slice(signature).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)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ use sp_core::{H256, Pair, Public, crypto::AccountId32, ed25519};
+ // 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 sp_runtime::{
+ Perbill, MultiSignature,
+ traits::{BlakeTwo256, IdentityLookup, Identity, Verify, IdentifyAccount, Dispatchable},
+ testing::Header
+ };
+ use frame_support::{
+ impl_outer_origin, impl_outer_dispatch, assert_ok, assert_noop, parameter_types,
+ ord_parameter_types, dispatch::DispatchError::BadOrigin,
+ };
+ use frame_support::traits::Currency;
+ use balances::Error as BalancesError;
+
+ impl_outer_origin! {
+ pub enum Origin for Test {}
+ }
+
+ impl_outer_dispatch! {
+ pub enum Call for Test where origin: Origin {
+ purchase::Purchase,
+ vesting::Vesting,
+ }
+ }
+
+ type AccountId = AccountId32;
+
+ // For testing the module, we construct most of a mock runtime. This means
+ // first constructing a configuration type (`Test`) which `impl`s each of the
+ // configuration traits of modules we want to use.
+ #[derive(Clone, Eq, PartialEq)]
+ pub struct Test;
+ parameter_types! {
+ pub const BlockHashCount: u32 = 250;
+ pub const MaximumBlockWeight: u32 = 4 * 1024 * 1024;
+ pub const MaximumBlockLength: u32 = 4 * 1024 * 1024;
+ pub const AvailableBlockRatio: Perbill = Perbill::from_percent(75);
+ }
+ impl system::Trait for Test {
+ type BaseCallFilter = ();
+ type Origin = Origin;
+ type Call = Call;
+ type Index = u64;
+ type BlockNumber = u64;
+ type Hash = H256;
+ type Hashing = BlakeTwo256;
+ type AccountId = AccountId;
+ type Lookup = IdentityLookup;
+ type Header = Header;
+ type Event = ();
+ type BlockHashCount = BlockHashCount;
+ type MaximumBlockWeight = MaximumBlockWeight;
+ type DbWeight = ();
+ type BlockExecutionWeight = ();
+ type ExtrinsicBaseWeight = ();
+ type MaximumExtrinsicWeight = MaximumBlockWeight;
+ type MaximumBlockLength = MaximumBlockLength;
+ type AvailableBlockRatio = AvailableBlockRatio;
+ type Version = ();
+ type ModuleToIndex = ();
+ type AccountData = balances::AccountData;
+ type OnNewAccount = ();
+ type OnKilledAccount = Balances;
+ type SystemWeightInfo = ();
+ }
+
+ parameter_types! {
+ pub const ExistentialDeposit: u64 = 1;
+ }
+
+ impl balances::Trait for Test {
+ type Balance = u64;
+ type Event = ();
+ type DustRemoval = ();
+ type ExistentialDeposit = ExistentialDeposit;
+ type AccountStore = System;
+ type WeightInfo = ();
+ }
+
+ parameter_types! {
+ pub const MinVestedTransfer: u64 = 0;
+ }
+
+ impl vesting::Trait for Test {
+ type Event = ();
+ type Currency = Balances;
+ type BlockNumberToBalance = Identity;
+ type MinVestedTransfer = MinVestedTransfer;
+ type WeightInfo = ();
+ }
+
+ parameter_types! {
+ pub const MaxStatementLength: usize = 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 Trait for Test {
+ type Event = ();
+ type Currency = Balances;
+ type VestingSchedule = Vesting;
+ type ValidityOrigin = system::EnsureSignedBy;
+ type ConfigurationOrigin = system::EnsureSignedBy;
+ type MaxStatementLength = MaxStatementLength;
+ type UnlockedProportion = UnlockedProportion;
+ type MaxUnlocked = MaxUnlocked;
+ }
+
+ type System = system::Module;
+ type Balances = balances::Module;
+ type Vesting = vesting::Module;
+ type Purchase = Module;
+
+ // 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 = 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(Origin::signed(configuration_origin()), statement).unwrap();
+ Purchase::set_unlock_block(Origin::signed(configuration_origin()), unlock_block).unwrap();
+ Purchase::set_payment_account(Origin::signed(configuration_origin()), payment_account()).unwrap();
+ Balances::make_free_balance_be(&payment_account(), 100_000);
+ }
+
+ type AccountPublic = ::Signer;
+
+ /// Helper function to generate a crypto pair from seed
+ fn get_from_seed(seed: &str) -> ::Public {
+ TPublic::Pair::from_string(&format!("//{}", seed), None)
+ .expect("static values are valid; qed")
+ .public()
+ }
+
+ /// Helper function to generate an account ID from seed
+ fn get_account_id_from_seed(seed: &str) -> AccountId where
+ AccountPublic: From<::Public>
+ {
+ AccountPublic::from(get_from_seed::(seed)).into_account()
+ }
+
+ fn alice() -> AccountId {
+ get_account_id_from_seed::("Alice")
+ }
+
+ fn alice_ed25519() -> AccountId {
+ get_account_id_from_seed::("Alice")
+ }
+
+ fn bob() -> AccountId {
+ get_account_id_from_seed::("Bob")
+ }
+
+ 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(Origin::signed(alice()), statement.clone()),
+ BadOrigin,
+ );
+ // Too Long
+ let long_statement = [0u8; 10_000].to_vec();
+ assert_noop!(
+ Purchase::set_statement(Origin::signed(configuration_origin()), long_statement),
+ Error::::InvalidStatement,
+ );
+ // Just right...
+ assert_ok!(Purchase::set_statement(Origin::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(Origin::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(Origin::signed(configuration_origin()), bad_unlock_block),
+ Error::::InvalidUnlockBlock,
+ );
+ // Just right...
+ assert_ok!(Purchase::set_unlock_block(Origin::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(Origin::signed(alice()), payment_account.clone()),
+ BadOrigin,
+ );
+ // Just right...
+ assert_ok!(Purchase::set_payment_account(Origin::signed(configuration_origin()), payment_account.clone()));
+ assert_eq!(PaymentAccount::::get(), 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(
+ Origin::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(Origin::signed(alice()), alice(), alice_signature().to_vec()),
+ BadOrigin,
+ );
+
+ // Wrong Account/Signature
+ assert_noop!(
+ Purchase::create_account(Origin::signed(validity_origin()), alice(), bob_signature().to_vec()),
+ Error::::InvalidSignature,
+ );
+
+ // Account with vesting
+ assert_ok!(::VestingSchedule::add_vesting_schedule(
+ &alice(),
+ 100,
+ 1,
+ 50
+ ));
+ assert_noop!(
+ Purchase::create_account(Origin::signed(validity_origin()), alice(), alice_signature().to_vec()),
+ Error::::VestingScheduleExists,
+ );
+
+ // Duplicate Purchasing Account
+ assert_ok!(
+ Purchase::create_account(Origin::signed(validity_origin()), bob(), bob_signature().to_vec())
+ );
+ assert_noop!(
+ Purchase::create_account(Origin::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(
+ Origin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec(),
+ ));
+ // She submits KYC, and we update the status to `Pending`.
+ assert_ok!(Purchase::update_validity_status(
+ Origin::signed(validity_origin()),
+ alice(),
+ AccountValidity::Pending,
+ ));
+ // KYC comes back negative, so we mark the account invalid.
+ assert_ok!(Purchase::update_validity_status(
+ Origin::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(
+ Origin::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(
+ Origin::signed(alice()),
+ alice(),
+ AccountValidity::Pending,
+ ), BadOrigin);
+ // Inactive Account
+ assert_noop!(Purchase::update_validity_status(
+ Origin::signed(validity_origin()),
+ alice(),
+ AccountValidity::Pending,
+ ), Error::::InvalidAccount);
+ // Already Completed
+ assert_ok!(Purchase::create_account(
+ Origin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec(),
+ ));
+ assert_ok!(Purchase::update_validity_status(
+ Origin::signed(validity_origin()),
+ alice(),
+ AccountValidity::Completed,
+ ));
+ assert_noop!(Purchase::update_validity_status(
+ Origin::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(
+ Origin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec())
+ );
+ // And approved for basic contribution
+ assert_ok!(Purchase::update_validity_status(
+ Origin::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(
+ Origin::signed(validity_origin()),
+ alice(),
+ 50,
+ 50,
+ Permill::from_rational_approximation(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(
+ Origin::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(
+ Origin::signed(alice()),
+ alice(),
+ 50,
+ 50,
+ Permill::zero(),
+ ), BadOrigin);
+ // Inactive Account
+ assert_noop!(Purchase::update_balance(
+ Origin::signed(validity_origin()),
+ alice(),
+ 50,
+ 50,
+ Permill::zero(),
+ ), Error::::InvalidAccount);
+ // Overflow
+ assert_noop!(Purchase::update_balance(
+ Origin::signed(validity_origin()),
+ alice(),
+ u64::max_value(),
+ u64::max_value(),
+ Permill::zero(),
+ ), Error::::InvalidAccount);
+ });
+ }
+
+ #[test]
+ fn payout_works() {
+ new_test_ext().execute_with(|| {
+ // Alice and Bob accounts are created
+ assert_ok!(Purchase::create_account(
+ Origin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec())
+ );
+ assert_ok!(Purchase::create_account(
+ Origin::signed(validity_origin()),
+ bob(),
+ bob_signature().to_vec())
+ );
+ // Alice is approved for basic contribution
+ assert_ok!(Purchase::update_validity_status(
+ Origin::signed(validity_origin()),
+ alice(),
+ AccountValidity::ValidLow,
+ ));
+ // Bob is approved for high contribution
+ assert_ok!(Purchase::update_validity_status(
+ Origin::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(
+ Origin::signed(validity_origin()),
+ alice(),
+ 50,
+ 50,
+ Permill::zero(),
+ ));
+ assert_ok!(Purchase::update_balance(
+ Origin::signed(validity_origin()),
+ bob(),
+ 100,
+ 150,
+ Permill::zero(),
+ ));
+ // Now we call payout for Alice and Bob.
+ assert_ok!(Purchase::payout(
+ Origin::signed(payment_account()),
+ alice(),
+ ));
+ assert_ok!(Purchase::payout(
+ Origin::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 = Call::Vesting(vesting::Call::::vest());
+ assert_ok!(vest_call.clone().dispatch(Origin::signed(alice())));
+ assert_ok!(vest_call.clone().dispatch(Origin::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(Origin::signed(alice())));
+ assert_ok!(vest_call.clone().dispatch(Origin::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(
+ Origin::signed(alice()),
+ alice(),
+ ), BadOrigin);
+ // Account with Existing Vesting Schedule
+ assert_ok!(::VestingSchedule::add_vesting_schedule(
+ &bob(), 100, 1, 50,
+ ));
+ assert_noop!(Purchase::payout(
+ Origin::signed(payment_account()),
+ bob(),
+ ), Error::::VestingScheduleExists);
+ // Invalid Account (never created)
+ assert_noop!(Purchase::payout(
+ Origin::signed(payment_account()),
+ alice(),
+ ), Error::::InvalidAccount);
+ // Invalid Account (created, but not valid)
+ assert_ok!(Purchase::create_account(
+ Origin::signed(validity_origin()),
+ alice(),
+ alice_signature().to_vec())
+ );
+ assert_noop!(Purchase::payout(
+ Origin::signed(payment_account()),
+ alice(),
+ ), Error::::InvalidAccount);
+ // Not enough funds in payment account
+ assert_ok!(Purchase::update_validity_status(
+ Origin::signed(validity_origin()),
+ alice(),
+ AccountValidity::ValidHigh,
+ ));
+ assert_ok!(Purchase::update_balance(
+ Origin::signed(validity_origin()),
+ alice(),
+ 100_000,
+ 100_000,
+ Permill::zero(),
+ ));
+ assert_noop!(Purchase::payout(
+ Origin::signed(payment_account()),
+ alice(),
+ ), BalancesError::::InsufficientBalance);
+ });
+ }
+}
diff --git a/runtime/polkadot/Cargo.toml b/runtime/polkadot/Cargo.toml
index 2c8373cb8314..fa6d8efb5dd6 100644
--- a/runtime/polkadot/Cargo.toml
+++ b/runtime/polkadot/Cargo.toml
@@ -68,7 +68,7 @@ frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch =
frame-system-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
pallet-offences-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
pallet-session-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "master", default-features = false, optional = true }
-hex-literal = { version = "0.2.1", optional = true }
+hex-literal = { version = "0.2.1" }
runtime-common = { package = "polkadot-runtime-common", path = "../common", default-features = false }
primitives = { package = "polkadot-primitives", path = "../../primitives", default-features = false }
@@ -163,7 +163,8 @@ runtime-benchmarks = [
"vesting/runtime-benchmarks",
"pallet-offences-benchmarking",
"pallet-session-benchmarking",
- "hex-literal",
+ # renable when optional
+ # "hex-literal",
]
# When enabled, the runtime api will not be build.
#
diff --git a/runtime/polkadot/src/lib.rs b/runtime/polkadot/src/lib.rs
index 177bf7e26e37..5e1e29262573 100644
--- a/runtime/polkadot/src/lib.rs
+++ b/runtime/polkadot/src/lib.rs
@@ -25,7 +25,7 @@ use runtime_common::{
impls::{CurrencyToVoteHandler, ToAuthor},
NegativeImbalance, BlockHashCount, MaximumBlockWeight, AvailableBlockRatio,
MaximumBlockLength, BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight,
- MaximumExtrinsicWeight,
+ MaximumExtrinsicWeight, purchase,
};
use sp_std::prelude::*;
@@ -57,11 +57,11 @@ use version::NativeVersion;
use sp_core::OpaqueMetadata;
use sp_staking::SessionIndex;
use frame_support::{
- parameter_types, construct_runtime, debug, RuntimeDebug,
+ parameter_types, ord_parameter_types, construct_runtime, debug, RuntimeDebug,
traits::{KeyOwnerProofSystem, SplitTwoWays, Randomness, LockIdentifier, Filter},
weights::Weight,
};
-use system::{EnsureRoot, EnsureOneOf};
+use system::{EnsureRoot, EnsureOneOf, EnsureSignedBy};
use im_online::sr25519::AuthorityId as ImOnlineId;
use authority_discovery_primitives::AuthorityId as AuthorityDiscoveryId;
use transaction_payment_rpc_runtime_api::RuntimeDispatchInfo;
@@ -135,7 +135,8 @@ impl Filter for BaseFilter {
Call::Session(_) | Call::FinalityTracker(_) | Call::Grandpa(_) | Call::ImOnline(_) |
Call::AuthorityDiscovery(_) |
Call::Utility(_) | Call::Claims(_) | Call::Vesting(_) | Call::Sudo(_) |
- Call::Identity(_) | Call::Proxy(_) | Call::Multisig(_) | Call::Poll(_) =>
+ Call::Identity(_) | Call::Proxy(_) | Call::Multisig(_) | Call::Poll(_) |
+ Call::Purchase(_) =>
true,
}
}
@@ -944,6 +945,46 @@ impl poll::Trait for Runtime {
type End = PollEnd;
}
+parameter_types! {
+ pub const MaxStatementLength: usize = 1_000;
+ pub const UnlockedProportion: Permill = Permill::zero();
+ pub const MaxUnlocked: Balance = 0;
+}
+
+ord_parameter_types! {
+ pub const W3FValidity: AccountId = AccountId::from(
+ // 142wAF65SK7PxhyzzrWz5m5PXDtooehgePBd7rc2NWpfc8Wa
+ hex_literal::hex!("862e432e0cf75693899c62691ac0f48967f815add97ae85659dcde8332708551")
+ );
+ pub const W3FConfiguration: AccountId = AccountId::from(
+ // 1KvKReVmUiTc2LW2a4qyHsaJJ9eE9LRsywZkMk5hyBeyHgw
+ hex_literal::hex!("0e6de68b13b82479fbe988ab9ecb16bad446b67b993cdd9198cd41c7c6259c49")
+ );
+}
+
+type ValidityOrigin = EnsureOneOf<
+ AccountId,
+ EnsureRoot,
+ EnsureSignedBy,
+>;
+
+type ConfigurationOrigin = EnsureOneOf<
+ AccountId,
+ EnsureRoot,
+ EnsureSignedBy,
+>;
+
+impl purchase::Trait for Runtime {
+ type Event = Event;
+ type Currency = Balances;
+ type VestingSchedule = Vesting;
+ type ValidityOrigin = ValidityOrigin;
+ type ConfigurationOrigin = ConfigurationOrigin;
+ type MaxStatementLength = MaxStatementLength;
+ type UnlockedProportion = UnlockedProportion;
+ type MaxUnlocked = MaxUnlocked;
+}
+
construct_runtime! {
pub enum Runtime where
Block = Block,
@@ -1012,6 +1053,9 @@ construct_runtime! {
// Poll module.
Poll: poll::{Module, Call, Storage, Event},
+
+ // DOT Purchase module. Late addition.
+ Purchase: purchase::{Module, Call, Storage, Event},
}
}