Browse Source

Merge pull request #1909 from shamil-gadelshin/membership_modify_module2

Modify Membership module.
Bedeho Mender 4 years ago
parent
commit
f749f63bf8

+ 3 - 3
node/src/chain_spec/initial_members.rs

@@ -1,13 +1,13 @@
-use node_runtime::{membership, AccountId, Moment};
+use node_runtime::{membership, AccountId};
 use std::{fs, path::Path};
 
 /// Generates a Vec of genesis members parsed from a json file
-pub fn from_json(data_file: &Path) -> Vec<membership::genesis::Member<u64, AccountId, Moment>> {
+pub fn from_json(data_file: &Path) -> Vec<membership::genesis::Member<u64, AccountId>> {
     let data = fs::read_to_string(data_file).expect("Failed reading file");
     serde_json::from_str(&data).expect("failed parsing members data")
 }
 
 /// Generates an empty Vec of genesis members
-pub fn none() -> Vec<membership::genesis::Member<u64, AccountId, Moment>> {
+pub fn none() -> Vec<membership::genesis::Member<u64, AccountId>> {
     vec![]
 }

+ 3 - 3
node/src/chain_spec/mod.rs

@@ -32,8 +32,8 @@ use node_runtime::{
     membership, wasm_binary_unwrap, AuthorityDiscoveryConfig, BabeConfig, Balance, BalancesConfig,
     ContentDirectoryConfig, CouncilConfig, CouncilElectionConfig, DataObjectStorageRegistryConfig,
     DataObjectTypeRegistryConfig, ElectionParameters, ForumConfig, GrandpaConfig, ImOnlineConfig,
-    MembersConfig, Moment, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig,
-    SudoConfig, SystemConfig, DAYS,
+    MembersConfig, SessionConfig, SessionKeys, Signature, StakerStatus, StakingConfig, SudoConfig,
+    SystemConfig, DAYS,
 };
 
 // Exported to be used by chain-spec-builder
@@ -206,7 +206,7 @@ pub fn testnet_genesis(
     )>,
     root_key: AccountId,
     endowed_accounts: Vec<AccountId>,
-    members: Vec<membership::genesis::Member<u64, AccountId, Moment>>,
+    members: Vec<membership::genesis::Member<u64, AccountId>>,
     forum_config: ForumConfig,
     initial_balances: Vec<(AccountId, Balance)>,
 ) -> GenesisConfig {

+ 6 - 4
runtime-modules/membership/src/genesis.rs

@@ -1,17 +1,19 @@
+//! Membership genesis module.
+
 #![cfg(feature = "std")]
 
 use crate::{GenesisConfig, Trait};
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Serialize, Deserialize)]
-pub struct Member<MemberId, AccountId, Moment> {
+pub struct Member<MemberId, AccountId> {
     pub member_id: MemberId,
     pub root_account: AccountId,
     pub controller_account: AccountId,
     pub handle: String,
     pub avatar_uri: String,
     pub about: String,
-    pub registered_at_time: Moment,
+    pub name: String,
 }
 
 /// Builder fo membership module genesis configuration.
@@ -33,7 +35,7 @@ impl<T: Trait> GenesisConfigBuilder<T> {
     }
 
     /// Generates a Vec of `Member`s from pairs of MemberId and AccountId
-    fn generate_mock_members(&self) -> Vec<Member<T::MemberId, T::AccountId, T::Moment>> {
+    fn generate_mock_members(&self) -> Vec<Member<T::MemberId, T::AccountId>> {
         self.members
             .iter()
             .enumerate()
@@ -45,7 +47,7 @@ impl<T: Trait> GenesisConfigBuilder<T> {
                 handle: (10000 + ix).to_string(),
                 avatar_uri: "".into(),
                 about: "".into(),
-                registered_at_time: T::Moment::from(0),
+                name: "".into(),
             })
             .collect()
     }

+ 276 - 193
runtime-modules/membership/src/lib.rs

@@ -1,3 +1,43 @@
+//! Joystream membership module.
+//!
+//! Memberships are the stable identifier under which actors occupy roles,
+//! submit proposals and communicate on the platform.
+//!
+//! ### Overview
+//! A membership is a representation of an actor on the platform,
+//! and it exist to serve the profile and reputation purposes.
+//!
+//! #### Profile
+//! A membership has an associated rich profile that includes information that support presenting
+//! the actor in a human friendly way in applications, much more so than raw accounts in isolation.
+//!
+//! #### Reputation
+//!
+//! Facilitates the consolidation of all activity under one stable identifier,
+//! allowing an actor to invest in the reputation of a membership through prolonged participation
+//! with good conduct. This gives honest and competent actors a practical way to signal quality,
+//! and this quality signal is a key screening parameter allowing entry into more important and
+//! sensitive activities. While nothing technically prevents an actor from registering for multiple
+//! memberships, the value of doing a range of activities under one membership should be greater
+//! than having it fragmented, since reputation, in essence, increases with the length and scope of
+//! the history of consistent good conduct.
+//!
+//! It's important to be aware that a membership is not an account, but a higher level concept that
+//! involves accounts for authentication. The membership subsystem is responsible for storing and
+//! managing all memberships on the platform, as well as enabling the creation of new memberships,
+//! and the terms under which this may happen.
+//!
+//! Supported extrinsics:
+//! - [update_profile](./struct.Module.html#method.update_profile) - updates profile parameters.
+//! - [buy_membership](./struct.Module.html#method.buy_membership) - allows to buy membership
+//! for non-members.
+//! - [update_accounts](./struct.Module.html#method.update_accounts) - updates member accounts.
+//! - [update_profile_verification](./struct.Module.html#method.update_profile_verification) -
+//! updates member profile verification status.
+//! - [set_referral_cut](./struct.Module.html#method.set_referral_cut) - updates the referral cut.
+//!
+//! [Joystream handbook description](https://joystream.gitbook.io/joystream-handbook/subsystems/membership)
+
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
 
@@ -7,8 +47,9 @@ mod tests;
 use codec::{Decode, Encode};
 use frame_support::traits::{Currency, Get};
 use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure};
+use frame_system::ensure_root;
 use frame_system::ensure_signed;
-use sp_arithmetic::traits::One;
+use sp_arithmetic::traits::{One, Zero};
 use sp_std::borrow::ToOwned;
 use sp_std::vec::Vec;
 
@@ -20,6 +61,7 @@ type BalanceOf<T> = <T as balances::Trait>::Balance;
 pub trait Trait:
     frame_system::Trait + balances::Trait + pallet_timestamp::Trait + common::Trait
 {
+    /// Membership module event type.
     type Event: From<Event<Self>> + Into<<Self as frame_system::Trait>::Event>;
 
     /// Defines the default membership fee.
@@ -34,35 +76,26 @@ const DEFAULT_MIN_HANDLE_LENGTH: u32 = 5;
 const DEFAULT_MAX_HANDLE_LENGTH: u32 = 40;
 const DEFAULT_MAX_AVATAR_URI_LENGTH: u32 = 1024;
 const DEFAULT_MAX_ABOUT_TEXT_LENGTH: u32 = 2048;
+const DEFAULT_MAX_NAME_LENGTH: u32 = 200;
 
-/// Public membership object alias.
-pub type Membership<T> = MembershipObject<
-    <T as frame_system::Trait>::BlockNumber,
-    <T as pallet_timestamp::Trait>::Moment,
-    <T as frame_system::Trait>::AccountId,
->;
+/// Public membership profile alias.
+pub type Membership<T> = MembershipObject<<T as frame_system::Trait>::AccountId>;
 
 #[derive(Encode, Decode, Default)]
-/// Stored information about a registered user
-pub struct MembershipObject<BlockNumber, Moment, AccountId> {
-    /// The unique handle chosen by member
+/// Stored information about a registered user.
+pub struct MembershipObject<AccountId> {
+    /// User name.
+    pub name: Vec<u8>,
+
+    /// The unique handle chosen by member.
     pub handle: Vec<u8>,
 
-    /// A Url to member's Avatar image
+    /// A Url to member's Avatar image.
     pub avatar_uri: Vec<u8>,
 
-    /// Short text chosen by member to share information about themselves
+    /// Short text chosen by member to share information about themselves.
     pub about: Vec<u8>,
 
-    /// Block number when member was registered
-    pub registered_at_block: BlockNumber,
-
-    /// Timestamp when member was registered
-    pub registered_at_time: Moment,
-
-    /// How the member was registered
-    pub entry: EntryMethod,
-
     /// Member's root account id. Only the root account is permitted to set a new root account
     /// and update the controller account. Other modules may only allow certain actions if
     /// signed with root account. It is intended to be an account that can remain offline and
@@ -81,60 +114,78 @@ pub struct MembershipObject<BlockNumber, Moment, AccountId> {
 
 // Contains valid or default user details
 struct ValidatedUserInfo {
+    name: Vec<u8>,
     handle: Vec<u8>,
     avatar_uri: Vec<u8>,
     about: Vec<u8>,
 }
 
-#[derive(Encode, Decode, Debug, PartialEq)]
-pub enum EntryMethod {
-    Paid,
-    Genesis,
-}
+/// Parameters for the buy_membership extrinsic.
+#[derive(Encode, Decode, Default, Clone, PartialEq, Debug)]
+pub struct BuyMembershipParameters<AccountId, MemberId> {
+    /// New member root account.
+    pub root_account: AccountId,
 
-/// Must be default constructible because it indirectly is a value in a storage map.
-/// ***SHOULD NEVER ACTUALLY GET CALLED, IS REQUIRED TO DUE BAD STORAGE MODEL IN SUBSTRATE***
-impl Default for EntryMethod {
-    fn default() -> Self {
-        Self::Genesis
-    }
+    /// New member controller account.
+    pub controller_account: AccountId,
+
+    /// New member user name.
+    pub name: Option<Vec<u8>>,
+
+    /// New member handle.
+    pub handle: Option<Vec<u8>>,
+
+    /// New member avatar URI.
+    pub avatar_uri: Option<Vec<u8>>,
+
+    /// New member 'about' text.
+    pub about: Option<Vec<u8>>,
+
+    /// Referrer member id.
+    pub referrer_id: Option<MemberId>,
 }
 
 decl_error! {
     /// Membership module predefined errors
     pub enum Error for Module<T: Trait> {
-        /// New members not allowed
+        /// New members not allowed.
         NewMembersNotAllowed,
 
-        /// Not enough balance to buy membership
+        /// Not enough balance to buy membership.
         NotEnoughBalanceToBuyMembership,
 
-        /// Controller account required
+        /// Controller account required.
         ControllerAccountRequired,
 
-        /// Root account required
+        /// Root account required.
         RootAccountRequired,
 
-        /// Invalid origin
+        /// Invalid origin.
         UnsignedOrigin,
 
         /// Member profile not found (invalid member id).
         MemberProfileNotFound,
 
-        /// Handle already registered
+        /// Handle already registered.
         HandleAlreadyRegistered,
 
-        /// Handle too short
+        /// Handle too short.
         HandleTooShort,
 
-        /// Handle too long
+        /// Handle too long.
         HandleTooLong,
 
-        /// Avatar uri too long
+        /// Avatar uri too long.
         AvatarUriTooLong,
 
-        /// Handle must be provided during registration
+        /// Name too long.
+        NameTooLong,
+
+        /// Handle must be provided during registration.
         HandleMustBeProvidedDuringRegistration,
+
+        /// Cannot find a membership for a provided referrer id.
+        ReferrerIsNotMember,
     }
 }
 
@@ -144,7 +195,7 @@ decl_storage! {
         /// total number of members created. MemberIds start at Zero.
         pub NextMemberId get(fn members_created) : T::MemberId;
 
-        /// Mapping of member's id to their membership profile
+        /// Mapping of member's id to their membership profile.
         pub MembershipById get(fn membership) : map hasher(blake2_128_concat)
             T::MemberId => Membership<T>;
 
@@ -152,30 +203,41 @@ decl_storage! {
         pub(crate) MemberIdsByRootAccountId : map hasher(blake2_128_concat)
             T::AccountId => Vec<T::MemberId>;
 
-        /// Mapping of a controller account id to vector of member ids it controls
+        /// Mapping of a controller account id to vector of member ids it controls.
         pub(crate) MemberIdsByControllerAccountId : map hasher(blake2_128_concat)
             T::AccountId => Vec<T::MemberId>;
 
-        /// Registered unique handles and their mapping to their owner
+        /// Registered unique handles and their mapping to their owner.
         pub MemberIdByHandle get(fn handles) : map hasher(blake2_128_concat)
             Vec<u8> => T::MemberId;
 
-        /// Is the platform is accepting new members or not
+        /// Is the platform is accepting new members or not.
         pub NewMembershipsAllowed get(fn new_memberships_allowed) : bool = true;
 
-        // User Input Validation parameters - do these really need to be state variables
-        // I don't see a need to adjust these in future?
+        /// Minimum allowed handle length.
         pub MinHandleLength get(fn min_handle_length) : u32 = DEFAULT_MIN_HANDLE_LENGTH;
+
+        /// Maximum allowed handle length.
         pub MaxHandleLength get(fn max_handle_length) : u32 = DEFAULT_MAX_HANDLE_LENGTH;
+
+        /// Maximum allowed avatar URI length.
         pub MaxAvatarUriLength get(fn max_avatar_uri_length) : u32 = DEFAULT_MAX_AVATAR_URI_LENGTH;
+
+        /// Maximum allowed 'about' text length.
         pub MaxAboutTextLength get(fn max_about_text_length) : u32 = DEFAULT_MAX_ABOUT_TEXT_LENGTH;
 
+        /// Maximum allowed name length.
+        pub MaxNameLength get(fn max_name_length) : u32 = DEFAULT_MAX_NAME_LENGTH;
+
+        /// Referral cut to receive during on buying the membership.
+        pub ReferralCut get(fn referral_cut) : BalanceOf<T>;
     }
     add_extra_genesis {
-        config(members) : Vec<genesis::Member<T::MemberId, T::AccountId, T::Moment>>;
+        config(members) : Vec<genesis::Member<T::MemberId, T::AccountId>>;
         build(|config: &GenesisConfig<T>| {
             for member in &config.members {
                 let checked_user_info = <Module<T>>::check_user_registration_info(
+                    Some(member.name.clone().into_bytes()),
                     Some(member.handle.clone().into_bytes()),
                     Some(member.avatar_uri.clone().into_bytes()),
                     Some(member.about.clone().into_bytes())
@@ -185,13 +247,8 @@ decl_storage! {
                     &member.root_account,
                     &member.controller_account,
                     &checked_user_info,
-                    EntryMethod::Genesis,
-                    T::BlockNumber::from(1),
-                    member.registered_at_time
                 ).expect("Importing Member Failed");
 
-
-
                 // ensure imported member id matches assigned id
                 assert_eq!(member_id, member.member_id, "Import Member Failed: MemberId Incorrect");
             }
@@ -201,16 +258,14 @@ decl_storage! {
 
 decl_event! {
     pub enum Event<T> where
-      <T as frame_system::Trait>::AccountId,
       <T as common::Trait>::MemberId,
+      Balance = BalanceOf<T>,
     {
-        MemberRegistered(MemberId, AccountId),
-        MemberUpdatedAboutText(MemberId),
-        MemberUpdatedAvatar(MemberId),
-        MemberUpdatedHandle(MemberId),
-        MemberSetRootAccount(MemberId, AccountId),
-        MemberSetControllerAccount(MemberId, AccountId),
+        MemberRegistered(MemberId),
+        MemberProfileUpdated(MemberId),
+        MemberAccountsUpdated(MemberId),
         MemberVerificationStatusUpdated(MemberId, bool),
+        ReferralCutUpdated(Balance),
     }
 }
 
@@ -221,154 +276,179 @@ decl_module! {
         /// Exports const - membership fee.
         const MembershipFee: BalanceOf<T> = T::MembershipFee::get();
 
-        /// Non-members can buy membership
+        /// Non-members can buy membership.
         #[weight = 10_000_000] // TODO: adjust weight
         pub fn buy_membership(
             origin,
-            handle: Option<Vec<u8>>,
-            avatar_uri: Option<Vec<u8>>,
-            about: Option<Vec<u8>>
+            params: BuyMembershipParameters<T::AccountId, T::MemberId>
         ) {
             let who = ensure_signed(origin)?;
 
-            // make sure we are accepting new memberships
+            // Make sure we are accepting new memberships.
             ensure!(Self::new_memberships_allowed(), Error::<T>::NewMembersNotAllowed);
 
             let fee = T::MembershipFee::get();
 
-            // ensure enough free balance to cover terms fees
+            // Ensure enough free balance to cover terms fees.
             ensure!(
                 balances::Module::<T>::usable_balance(&who) >= fee,
                 Error::<T>::NotEnoughBalanceToBuyMembership
             );
 
-            let user_info = Self::check_user_registration_info(handle, avatar_uri, about)?;
+            // Verify user parameters.
+            let user_info = Self::check_user_registration_info(
+                params.name,
+                params.handle,
+                params.avatar_uri,
+                params.about)
+            ?;
+
+            let referrer = params
+                .referrer_id
+                .map(|referrer_id| {
+                    Self::ensure_membership_with_error(referrer_id, Error::<T>::ReferrerIsNotMember)
+                })
+                .transpose()?;
+
+            //
+            // == MUTATION SAFE ==
+            //
 
             let member_id = Self::insert_member(
-                &who,
-                &who,
+                &params.root_account,
+                &params.controller_account,
                 &user_info,
-                EntryMethod::Paid,
-                <frame_system::Module<T>>::block_number(),
-                <pallet_timestamp::Module<T>>::now()
             )?;
 
+            // Collect membership fee (just burn it).
             let _ = balances::Module::<T>::slash(&who, fee);
 
-            Self::deposit_event(RawEvent::MemberRegistered(member_id, who));
-        }
+            // Reward the referring member.
+            if let Some(referrer) = referrer{
+                let referral_cut: BalanceOf<T> = Self::get_referral_bonus();
 
-        /// Change member's about text
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn change_member_about_text(origin, member_id: T::MemberId, text: Vec<u8>) {
-            let sender = ensure_signed(origin)?;
-
-            let membership = Self::ensure_membership(member_id)?;
-
-            ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
-
-            Self::_change_member_about_text(member_id, &text)?;
-        }
-
-        /// Change member's avatar
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn change_member_avatar(origin, member_id: T::MemberId, uri: Vec<u8>) {
-            let sender = ensure_signed(origin)?;
-
-            let membership = Self::ensure_membership(member_id)?;
-
-            ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
-
-            Self::_change_member_avatar(member_id, &uri)?;
-        }
-
-        /// Change member's handle. Will ensure new handle is unique and old one will be available
-        /// for other members to use.
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn change_member_handle(origin, member_id: T::MemberId, handle: Vec<u8>) {
-            let sender = ensure_signed(origin)?;
-
-            let membership = Self::ensure_membership(member_id)?;
-
-            ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
+                if referral_cut > Zero::zero() {
+                    let _ = balances::Module::<T>::deposit_creating(
+                        &referrer.controller_account,
+                        referral_cut
+                    );
+                }
+            }
 
-            Self::_change_member_handle(member_id, handle)?;
+            // Fire the event.
+            Self::deposit_event(RawEvent::MemberRegistered(member_id));
         }
 
-        /// Update member's all or some of handle, avatar and about text.
+        /// Update member's all or some of name, handle, avatar and about text.
+        /// No effect if no changed fields.
         #[weight = 10_000_000] // TODO: adjust weight
-        pub fn update_membership(
+        pub fn update_profile(
             origin,
             member_id: T::MemberId,
+            name: Option<Vec<u8>>,
             handle: Option<Vec<u8>>,
             avatar_uri: Option<Vec<u8>>,
             about: Option<Vec<u8>>
         ) {
-            let sender = ensure_signed(origin)?;
+            // No effect if no changes.
+            if name.is_none() && handle.is_none() && avatar_uri.is_none() && about.is_none() {
+                return Ok(())
+            }
+
+            Self::ensure_member_controller_account_signed(origin, &member_id)?;
 
-            let membership = Self::ensure_membership(member_id)?;
+            let mut membership = Self::ensure_membership(member_id)?;
 
-            ensure!(membership.controller_account == sender, Error::<T>::ControllerAccountRequired);
+            // Prepare for possible handle change;
+            let old_handle = membership.handle.clone();
+            let mut new_handle: Option<Vec<u8>> = None;
 
+            // Update fields if needed
             if let Some(uri) = avatar_uri {
-                Self::_change_member_avatar(member_id, &uri)?;
+                Self::validate_avatar(&uri)?;
+                membership.avatar_uri = uri;
+            }
+            if let Some(name) = name {
+                Self::validate_name(&name)?;
+                membership.name = name;
             }
             if let Some(about) = about {
-                Self::_change_member_about_text(member_id, &about)?;
+                let text = Self::validate_text(&about);
+                membership.about = text;
             }
             if let Some(handle) = handle {
-                Self::_change_member_handle(member_id, handle)?;
+                Self::validate_handle(&handle)?;
+                Self::ensure_unique_handle(&handle)?;
+
+                new_handle = Some(handle.clone());
+                membership.handle = handle;
             }
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <MembershipById<T>>::insert(member_id, membership);
+
+            if let Some(new_handle) = new_handle {
+                <MemberIdByHandle<T>>::remove(&old_handle);
+                <MemberIdByHandle<T>>::insert(new_handle, member_id);
+            }
+
+            Self::deposit_event(RawEvent::MemberProfileUpdated(member_id));
         }
 
+        /// Updates member root or controller accounts. No effect if both new accounts are empty.
         #[weight = 10_000_000] // TODO: adjust weight
-        pub fn set_controller_account(origin, member_id: T::MemberId, new_controller_account: T::AccountId) {
-            let sender = ensure_signed(origin)?;
+        pub fn update_accounts(
+            origin,
+            member_id: T::MemberId,
+            new_root_account: Option<T::AccountId>,
+            new_controller_account: Option<T::AccountId>,
+        ) {
+            // No effect if no changes.
+            if new_root_account.is_none() && new_controller_account.is_none() {
+                return Ok(())
+            }
 
+            let sender = ensure_signed(origin)?;
             let mut membership = Self::ensure_membership(member_id)?;
 
             ensure!(membership.root_account == sender, Error::<T>::RootAccountRequired);
 
-            // only update if new_controller_account is different than current one
-            if membership.controller_account != new_controller_account {
-                <MemberIdsByControllerAccountId<T>>::mutate(&membership.controller_account, |ids| {
+            //
+            // == MUTATION SAFE ==
+            //
+
+            if let Some(root_account) = new_root_account {
+                <MemberIdsByRootAccountId<T>>::mutate(&membership.root_account, |ids| {
                     ids.retain(|id| *id != member_id);
                 });
 
-                <MemberIdsByControllerAccountId<T>>::mutate(&new_controller_account, |ids| {
+                <MemberIdsByRootAccountId<T>>::mutate(&root_account, |ids| {
                     ids.push(member_id);
                 });
 
-                membership.controller_account = new_controller_account.clone();
-                <MembershipById<T>>::insert(member_id, membership);
-                Self::deposit_event(RawEvent::MemberSetControllerAccount(member_id, new_controller_account));
+                membership.root_account = root_account;
             }
-        }
-
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn set_root_account(origin, member_id: T::MemberId, new_root_account: T::AccountId) {
-            let sender = ensure_signed(origin)?;
 
-            let mut membership = Self::ensure_membership(member_id)?;
-
-            ensure!(membership.root_account == sender, Error::<T>::RootAccountRequired);
-
-            // only update if new root account is different than current one
-            if membership.root_account != new_root_account {
-                <MemberIdsByRootAccountId<T>>::mutate(&membership.root_account, |ids| {
+            if let Some(controller_account) = new_controller_account {
+                <MemberIdsByControllerAccountId<T>>::mutate(&membership.controller_account, |ids| {
                     ids.retain(|id| *id != member_id);
                 });
 
-                <MemberIdsByRootAccountId<T>>::mutate(&new_root_account, |ids| {
+                <MemberIdsByControllerAccountId<T>>::mutate(&controller_account, |ids| {
                     ids.push(member_id);
                 });
 
-                membership.root_account = new_root_account.clone();
-                <MembershipById<T>>::insert(member_id, membership);
-                Self::deposit_event(RawEvent::MemberSetRootAccount(member_id, new_root_account));
+                membership.controller_account = controller_account;
             }
+
+            <MembershipById<T>>::insert(member_id, membership);
+            Self::deposit_event(RawEvent::MemberAccountsUpdated(member_id));
         }
 
+        /// Updates member profile verification status. Requires working group member origin.
         #[weight = 10_000_000] // TODO: adjust weight
         pub fn update_profile_verification(
             origin,
@@ -392,16 +472,41 @@ decl_module! {
                 RawEvent::MemberVerificationStatusUpdated(target_member_id, is_verified)
             );
         }
+
+        /// Updates membership referral cut. Requires root origin.
+        #[weight = 10_000_000] // TODO: adjust weight
+        pub fn set_referral_cut(
+            origin,
+            value: BalanceOf<T>
+        ) {
+            ensure_root(origin)?;
+
+            //
+            // == MUTATION SAFE ==
+            //
+
+            <ReferralCut<T>>::put(value);
+
+            Self::deposit_event(RawEvent::ReferralCutUpdated(value));
+        }
     }
 }
 
 impl<T: Trait> Module<T> {
     /// Provided that the member_id exists return its membership. Returns error otherwise.
-    pub fn ensure_membership(id: T::MemberId) -> Result<Membership<T>, Error<T>> {
+    pub fn ensure_membership(member_id: T::MemberId) -> Result<Membership<T>, Error<T>> {
+        Self::ensure_membership_with_error(member_id, Error::<T>::MemberProfileNotFound)
+    }
+
+    /// Provided that the member_id exists return its membership. Returns provided error otherwise.
+    fn ensure_membership_with_error(
+        id: T::MemberId,
+        error: Error<T>,
+    ) -> Result<Membership<T>, Error<T>> {
         if <MembershipById<T>>::contains_key(&id) {
             Ok(Self::membership(&id))
         } else {
-            Err(Error::<T>::MemberProfileNotFound)
+            Err(error)
         }
     }
 
@@ -429,6 +534,7 @@ impl<T: Trait> Module<T> {
             || <MemberIdsByControllerAccountId<T>>::contains_key(who)
     }
 
+    // Ensure possible member handle is unique.
     #[allow(clippy::ptr_arg)] // cannot change to the "&[u8]" suggested by clippy
     fn ensure_unique_handle(handle: &Vec<u8>) -> Result<(), Error<T>> {
         ensure!(
@@ -438,6 +544,7 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
+    // Provides possible handle validation.
     fn validate_handle(handle: &[u8]) -> Result<(), Error<T>> {
         ensure!(
             handle.len() >= Self::min_handle_length() as usize,
@@ -450,12 +557,14 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
+    // Provides possible member about text validation.
     fn validate_text(text: &[u8]) -> Vec<u8> {
         let mut text = text.to_owned();
         text.truncate(Self::max_about_text_length() as usize);
         text
     }
 
+    // Provides possible member avatar uri validation.
     fn validate_avatar(uri: &[u8]) -> Result<(), Error<T>> {
         ensure!(
             uri.len() <= Self::max_avatar_uri_length() as usize,
@@ -464,8 +573,18 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
-    /// Basic user input validation
+    // Provides possible member name validation.
+    fn validate_name(name: &[u8]) -> Result<(), Error<T>> {
+        ensure!(
+            name.len() <= Self::max_name_length() as usize,
+            Error::<T>::NameTooLong
+        );
+        Ok(())
+    }
+
+    // Basic user input validation
     fn check_user_registration_info(
+        name: Option<Vec<u8>>,
         handle: Option<Vec<u8>>,
         avatar_uri: Option<Vec<u8>>,
         about: Option<Vec<u8>>,
@@ -477,33 +596,32 @@ impl<T: Trait> Module<T> {
         let about = Self::validate_text(&about.unwrap_or_default());
         let avatar_uri = avatar_uri.unwrap_or_default();
         Self::validate_avatar(&avatar_uri)?;
+        let name = name.unwrap_or_default();
+        Self::validate_name(&name)?;
 
         Ok(ValidatedUserInfo {
+            name,
             handle,
             avatar_uri,
             about,
         })
     }
 
+    // Inserts a member using a validated information. Sets handle, accounts caches.
     fn insert_member(
         root_account: &T::AccountId,
         controller_account: &T::AccountId,
         user_info: &ValidatedUserInfo,
-        entry_method: EntryMethod,
-        registered_at_block: T::BlockNumber,
-        registered_at_time: T::Moment,
     ) -> Result<T::MemberId, Error<T>> {
         Self::ensure_unique_handle(&user_info.handle)?;
 
         let new_member_id = Self::members_created();
 
         let membership: Membership<T> = MembershipObject {
+            name: user_info.name.clone(),
             handle: user_info.handle.clone(),
             avatar_uri: user_info.avatar_uri.clone(),
             about: user_info.about.clone(),
-            registered_at_block,
-            registered_at_time,
-            entry: entry_method,
             root_account: root_account.clone(),
             controller_account: controller_account.clone(),
             verified: false,
@@ -523,36 +641,7 @@ impl<T: Trait> Module<T> {
         Ok(new_member_id)
     }
 
-    fn _change_member_about_text(id: T::MemberId, text: &[u8]) -> Result<(), Error<T>> {
-        let mut membership = Self::ensure_membership(id)?;
-        let text = Self::validate_text(text);
-        membership.about = text;
-        Self::deposit_event(RawEvent::MemberUpdatedAboutText(id));
-        <MembershipById<T>>::insert(id, membership);
-        Ok(())
-    }
-
-    fn _change_member_avatar(id: T::MemberId, uri: &[u8]) -> Result<(), Error<T>> {
-        let mut membership = Self::ensure_membership(id)?;
-        Self::validate_avatar(uri)?;
-        membership.avatar_uri = uri.to_owned();
-        Self::deposit_event(RawEvent::MemberUpdatedAvatar(id));
-        <MembershipById<T>>::insert(id, membership);
-        Ok(())
-    }
-
-    fn _change_member_handle(id: T::MemberId, handle: Vec<u8>) -> Result<(), Error<T>> {
-        let mut membership = Self::ensure_membership(id)?;
-        Self::validate_handle(&handle)?;
-        Self::ensure_unique_handle(&handle)?;
-        <MemberIdByHandle<T>>::remove(&membership.handle);
-        <MemberIdByHandle<T>>::insert(handle.clone(), id);
-        membership.handle = handle;
-        Self::deposit_event(RawEvent::MemberUpdatedHandle(id));
-        <MembershipById<T>>::insert(id, membership);
-        Ok(())
-    }
-
+    /// Ensure origin corresponds to the controller account of the member.
     pub fn ensure_member_controller_account_signed(
         origin: T::Origin,
         member_id: &T::MemberId,
@@ -571,6 +660,7 @@ impl<T: Trait> Module<T> {
         Ok(signer_account)
     }
 
+    /// Validates that a member has the controller account.
     pub fn ensure_member_controller_account(
         signer_account: &T::AccountId,
         member_id: &T::MemberId,
@@ -586,18 +676,11 @@ impl<T: Trait> Module<T> {
         Ok(())
     }
 
-    pub fn ensure_member_root_account(
-        signer_account: &T::AccountId,
-        member_id: &T::MemberId,
-    ) -> Result<(), Error<T>> {
-        // Ensure member exists
-        let membership = Self::ensure_membership(*member_id)?;
+    // Calculate current referral bonus. It minimum between membership fee and referral cut.
+    pub(crate) fn get_referral_bonus() -> BalanceOf<T> {
+        let membership_fee = T::MembershipFee::get();
+        let referral_cut = Self::referral_cut();
 
-        ensure!(
-            membership.root_account == *signer_account,
-            Error::<T>::RootAccountRequired
-        );
-
-        Ok(())
+        membership_fee.min(referral_cut)
     }
 }

+ 141 - 20
runtime-modules/membership/src/tests/fixtures.rs

@@ -1,4 +1,5 @@
 use super::mock::*;
+use crate::BuyMembershipParameters;
 use frame_support::dispatch::DispatchResult;
 use frame_support::traits::{OnFinalize, OnInitialize};
 use frame_support::StorageMap;
@@ -50,6 +51,7 @@ pub fn assert_dispatch_error_message(result: DispatchResult, expected_result: Di
 
 #[derive(Clone, Debug, PartialEq)]
 pub struct TestUserInfo {
+    pub name: Option<Vec<u8>>,
     pub handle: Option<Vec<u8>>,
     pub avatar_uri: Option<Vec<u8>>,
     pub about: Option<Vec<u8>>,
@@ -57,38 +59,39 @@ pub struct TestUserInfo {
 
 pub fn get_alice_info() -> TestUserInfo {
     TestUserInfo {
-        handle: Some(String::from("alice").as_bytes().to_vec()),
-        avatar_uri: Some(
-            String::from("http://avatar-url.com/alice")
-                .as_bytes()
-                .to_vec(),
-        ),
-        about: Some(String::from("my name is alice").as_bytes().to_vec()),
+        name: Some(b"Alice".to_vec()),
+        handle: Some(b"alice".to_vec()),
+        avatar_uri: Some(b"http://avatar-url.com/alice".to_vec()),
+        about: Some(b"my name is alice".to_vec()),
     }
 }
 
 pub fn get_bob_info() -> TestUserInfo {
     TestUserInfo {
-        handle: Some(String::from("bobby").as_bytes().to_vec()),
-        avatar_uri: Some(
-            String::from("http://avatar-url.com/bob")
-                .as_bytes()
-                .to_vec(),
-        ),
-        about: Some(String::from("my name is bob").as_bytes().to_vec()),
+        name: Some(b"Bob".to_vec()),
+        handle: Some(b"bobby".to_vec()),
+        avatar_uri: Some(b"http://avatar-url.com/bob".to_vec()),
+        about: Some(b"my name is bob".to_vec()),
     }
 }
 
 pub const ALICE_ACCOUNT_ID: u64 = 1;
+pub const BOB_ACCOUNT_ID: u64 = 2;
 
 pub fn buy_default_membership_as_alice() -> DispatchResult {
     let info = get_alice_info();
-    Membership::buy_membership(
-        Origin::signed(ALICE_ACCOUNT_ID),
-        info.handle,
-        info.avatar_uri,
-        info.about,
-    )
+
+    let params = BuyMembershipParameters {
+        root_account: ALICE_ACCOUNT_ID,
+        controller_account: ALICE_ACCOUNT_ID,
+        name: info.name,
+        handle: info.handle,
+        avatar_uri: info.avatar_uri,
+        about: info.about,
+        referrer_id: None,
+    };
+
+    Membership::buy_membership(Origin::signed(ALICE_ACCOUNT_ID), params)
 }
 
 pub fn set_alice_free_balance(balance: u64) {
@@ -142,3 +145,121 @@ impl UpdateMembershipVerificationFixture {
         Self { worker_id, ..self }
     }
 }
+
+pub struct BuyMembershipFixture {
+    pub origin: RawOrigin<u64>,
+    pub root_account: u64,
+    pub controller_account: u64,
+    pub name: Option<Vec<u8>>,
+    pub handle: Option<Vec<u8>>,
+    pub avatar_uri: Option<Vec<u8>>,
+    pub about: Option<Vec<u8>>,
+    pub referrer_id: Option<u64>,
+}
+
+impl Default for BuyMembershipFixture {
+    fn default() -> Self {
+        let alice = get_alice_info();
+        Self {
+            origin: RawOrigin::Signed(ALICE_ACCOUNT_ID),
+            root_account: ALICE_ACCOUNT_ID,
+            controller_account: ALICE_ACCOUNT_ID,
+            name: alice.name,
+            handle: alice.handle,
+            avatar_uri: alice.avatar_uri,
+            about: alice.about,
+            referrer_id: None,
+        }
+    }
+}
+
+impl BuyMembershipFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let params = BuyMembershipParameters {
+            root_account: self.root_account.clone(),
+            controller_account: self.controller_account.clone(),
+            name: self.name.clone(),
+            handle: self.handle.clone(),
+            avatar_uri: self.avatar_uri.clone(),
+            about: self.about.clone(),
+            referrer_id: self.referrer_id.clone(),
+        };
+
+        let actual_result = Membership::buy_membership(self.origin.clone().into(), params);
+
+        assert_eq!(expected_result, actual_result);
+    }
+
+    pub fn with_referrer_id(self, referrer_id: u64) -> Self {
+        Self {
+            referrer_id: Some(referrer_id),
+            ..self
+        }
+    }
+
+    pub fn with_name(self, name: Vec<u8>) -> Self {
+        Self {
+            name: Some(name),
+            ..self
+        }
+    }
+
+    pub fn with_handle(self, handle: Vec<u8>) -> Self {
+        Self {
+            handle: Some(handle),
+            ..self
+        }
+    }
+
+    pub fn with_accounts(self, account_id: u64) -> Self {
+        Self {
+            root_account: account_id,
+            controller_account: account_id,
+            ..self
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}
+
+pub(crate) fn increase_total_balance_issuance_using_account_id(account_id: u64, balance: u64) {
+    let initial_balance = Balances::total_issuance();
+    {
+        let _ = Balances::deposit_creating(&account_id, balance);
+    }
+    assert_eq!(Balances::total_issuance(), initial_balance + balance);
+}
+
+pub struct SetReferralCutFixture {
+    pub origin: RawOrigin<u64>,
+    pub value: u64,
+}
+
+pub const DEFAULT_REFERRAL_CUT_VALUE: u64 = 100;
+
+impl Default for SetReferralCutFixture {
+    fn default() -> Self {
+        Self {
+            origin: RawOrigin::Root,
+            value: DEFAULT_REFERRAL_CUT_VALUE,
+        }
+    }
+}
+
+impl SetReferralCutFixture {
+    pub fn call_and_assert(&self, expected_result: DispatchResult) {
+        let actual_result = Membership::set_referral_cut(self.origin.clone().into(), self.value);
+
+        assert_eq!(expected_result, actual_result);
+
+        if actual_result.is_ok() {
+            assert_eq!(Membership::referral_cut(), self.value);
+        }
+    }
+
+    pub fn with_origin(self, origin: RawOrigin<u64>) -> Self {
+        Self { origin, ..self }
+    }
+}

+ 12 - 2
runtime-modules/membership/src/tests/mock.rs

@@ -16,8 +16,6 @@ use sp_runtime::{
 
 pub(crate) type MembershipWorkingGroupInstance = working_group::Instance4;
 
-pub use common::currency::GovernanceCurrency;
-
 impl_outer_origin! {
     pub enum Origin for Test {}
 }
@@ -287,6 +285,18 @@ pub fn build_test_externalities() -> sp_io::TestExternalities {
     TestExternalitiesBuilder::<Test>::default().build()
 }
 
+pub fn build_test_externalities_with_initial_members(
+    initial_members: Vec<(u64, u64)>,
+) -> sp_io::TestExternalities {
+    TestExternalitiesBuilder::<Test>::default()
+        .set_membership_config(
+            crate::genesis::GenesisConfigBuilder::default()
+                .members(initial_members)
+                .build(),
+        )
+        .build()
+}
+
 pub type Balances = balances::Module<Test>;
 pub type Membership = crate::Module<Test>;
 pub type System = frame_system::Module<Test>;

+ 291 - 170
runtime-modules/membership/src/tests/mod.rs

@@ -3,7 +3,6 @@
 pub(crate) mod fixtures;
 pub(crate) mod mock;
 
-use super::genesis;
 use crate::{Error, Event};
 use fixtures::*;
 use mock::*;
@@ -14,206 +13,232 @@ use frame_system::RawOrigin;
 use sp_runtime::DispatchError;
 
 #[test]
-fn buy_membership() {
-    const SURPLUS_BALANCE: u64 = 500;
+fn buy_membership_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
 
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(genesis::GenesisConfigBuilder::default().build())
-        .build()
-        .execute_with(|| {
-            let initial_balance = MembershipFee::get() + SURPLUS_BALANCE;
-            set_alice_free_balance(initial_balance);
+        let initial_balance = MembershipFee::get();
+        set_alice_free_balance(initial_balance);
 
-            let next_member_id = Membership::members_created();
+        let next_member_id = Membership::members_created();
 
-            assert_ok!(buy_default_membership_as_alice());
+        assert_ok!(buy_default_membership_as_alice());
 
-            let member_ids = vec![0];
-            assert_eq!(member_ids, vec![next_member_id]);
+        let member_ids = vec![0];
+        assert_eq!(member_ids, vec![next_member_id]);
 
-            let profile = get_membership_by_id(next_member_id);
+        let profile = get_membership_by_id(next_member_id);
 
-            assert_eq!(Some(profile.handle), get_alice_info().handle);
-            assert_eq!(Some(profile.avatar_uri), get_alice_info().avatar_uri);
-            assert_eq!(Some(profile.about), get_alice_info().about);
+        assert_eq!(Some(profile.name), get_alice_info().name);
+        assert_eq!(Some(profile.handle), get_alice_info().handle);
+        assert_eq!(Some(profile.avatar_uri), get_alice_info().avatar_uri);
+        assert_eq!(Some(profile.about), get_alice_info().about);
 
-            assert_eq!(Balances::free_balance(&ALICE_ACCOUNT_ID), SURPLUS_BALANCE);
+        // controller account initially set to primary account
+        assert_eq!(profile.controller_account, ALICE_ACCOUNT_ID);
+        assert_eq!(
+            <crate::MemberIdsByControllerAccountId<Test>>::get(ALICE_ACCOUNT_ID),
+            vec![next_member_id]
+        );
 
-            // controller account initially set to primary account
-            assert_eq!(profile.controller_account, ALICE_ACCOUNT_ID);
-            assert_eq!(
-                <crate::MemberIdsByControllerAccountId<Test>>::get(ALICE_ACCOUNT_ID),
-                vec![next_member_id]
-            );
-        });
+        EventFixture::assert_last_crate_event(Event::<Test>::MemberRegistered(next_member_id));
+    });
 }
 
 #[test]
 fn buy_membership_fails_without_enough_balance() {
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(genesis::GenesisConfigBuilder::default().build())
-        .build()
-        .execute_with(|| {
-            let initial_balance = MembershipFee::get() - 1;
-            set_alice_free_balance(initial_balance);
-
-            assert_dispatch_error_message(
-                buy_default_membership_as_alice(),
-                Err(Error::<Test>::NotEnoughBalanceToBuyMembership.into()),
-            );
-        });
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get() - 1;
+        set_alice_free_balance(initial_balance);
+
+        assert_dispatch_error_message(
+            buy_default_membership_as_alice(),
+            Err(Error::<Test>::NotEnoughBalanceToBuyMembership.into()),
+        );
+    });
 }
 
 #[test]
 fn buy_membership_fails_without_enough_balance_with_locked_balance() {
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(genesis::GenesisConfigBuilder::default().build())
-        .build()
-        .execute_with(|| {
-            let initial_balance = MembershipFee::get();
-            let lock_id = LockIdentifier::default();
-            Balances::set_lock(lock_id, &ALICE_ACCOUNT_ID, 1, WithdrawReasons::all());
-            set_alice_free_balance(initial_balance);
-
-            assert_dispatch_error_message(
-                buy_default_membership_as_alice(),
-                Err(Error::<Test>::NotEnoughBalanceToBuyMembership.into()),
-            );
-        });
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get();
+        let lock_id = LockIdentifier::default();
+        Balances::set_lock(lock_id, &ALICE_ACCOUNT_ID, 1, WithdrawReasons::all());
+        set_alice_free_balance(initial_balance);
+
+        assert_dispatch_error_message(
+            buy_default_membership_as_alice(),
+            Err(Error::<Test>::NotEnoughBalanceToBuyMembership.into()),
+        );
+    });
 }
 
 #[test]
-fn new_memberships_allowed_flag() {
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(genesis::GenesisConfigBuilder::default().build())
-        .build()
-        .execute_with(|| {
-            let initial_balance = MembershipFee::get() + 1;
-            set_alice_free_balance(initial_balance);
-
-            crate::NewMembershipsAllowed::put(false);
-
-            assert_dispatch_error_message(
-                buy_default_membership_as_alice(),
-                Err(Error::<Test>::NewMembersNotAllowed.into()),
-            );
-        });
+fn new_memberships_allowed_flag_works() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get() + 1;
+        set_alice_free_balance(initial_balance);
+
+        crate::NewMembershipsAllowed::put(false);
+
+        assert_dispatch_error_message(
+            buy_default_membership_as_alice(),
+            Err(Error::<Test>::NewMembersNotAllowed.into()),
+        );
+    });
 }
 
 #[test]
-fn unique_handles() {
-    const SURPLUS_BALANCE: u64 = 500;
-
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(genesis::GenesisConfigBuilder::default().build())
-        .build()
-        .execute_with(|| {
-            let initial_balance = MembershipFee::get() + SURPLUS_BALANCE;
-            set_alice_free_balance(initial_balance);
-
-            // alice's handle already taken
-            <crate::MemberIdByHandle<Test>>::insert(get_alice_info().handle.unwrap(), 1);
-
-            // should not be allowed to buy membership with that handle
-            assert_dispatch_error_message(
-                buy_default_membership_as_alice(),
-                Err(Error::<Test>::HandleAlreadyRegistered.into()),
-            );
-        });
+fn buy_membership_fails_with_non_unique_handle() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get();
+        set_alice_free_balance(initial_balance);
+
+        // alice's handle already taken
+        <crate::MemberIdByHandle<Test>>::insert(get_alice_info().handle.unwrap(), 1);
+
+        // should not be allowed to buy membership with that handle
+        assert_dispatch_error_message(
+            buy_default_membership_as_alice(),
+            Err(Error::<Test>::HandleAlreadyRegistered.into()),
+        );
+    });
 }
 
 #[test]
-fn update_profile() {
-    const SURPLUS_BALANCE: u64 = 500;
-
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(genesis::GenesisConfigBuilder::default().build())
-        .build()
-        .execute_with(|| {
-            let initial_balance = MembershipFee::get() + SURPLUS_BALANCE;
-            set_alice_free_balance(initial_balance);
-
-            let next_member_id = Membership::members_created();
-
-            assert_ok!(buy_default_membership_as_alice());
-            let info = get_bob_info();
-            assert_ok!(Membership::update_membership(
-                Origin::signed(ALICE_ACCOUNT_ID),
-                next_member_id,
-                info.handle,
-                info.avatar_uri,
-                info.about,
-            ));
-
-            let profile = get_membership_by_id(next_member_id);
-
-            assert_eq!(Some(profile.handle), get_bob_info().handle);
-            assert_eq!(Some(profile.avatar_uri), get_bob_info().avatar_uri);
-            assert_eq!(Some(profile.about), get_bob_info().about);
-        });
+fn update_profile_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        let initial_balance = MembershipFee::get();
+        set_alice_free_balance(initial_balance);
+
+        let next_member_id = Membership::members_created();
+
+        assert_ok!(buy_default_membership_as_alice());
+        let info = get_bob_info();
+        assert_ok!(Membership::update_profile(
+            Origin::signed(ALICE_ACCOUNT_ID),
+            next_member_id,
+            info.name,
+            info.handle,
+            info.avatar_uri,
+            info.about,
+        ));
+
+        let profile = get_membership_by_id(next_member_id);
+
+        assert_eq!(Some(profile.name), get_bob_info().name);
+        assert_eq!(Some(profile.handle), get_bob_info().handle);
+        assert_eq!(Some(profile.avatar_uri), get_bob_info().avatar_uri);
+        assert_eq!(Some(profile.about), get_bob_info().about);
+
+        assert!(<crate::MemberIdByHandle<Test>>::contains_key(
+            get_bob_info().handle.unwrap()
+        ));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::MemberProfileUpdated(next_member_id));
+    });
 }
 
 #[test]
-fn set_controller_key() {
-    let initial_members = [(0, ALICE_ACCOUNT_ID)];
-    const ALICE_CONTROLLER_ID: u64 = 2;
-
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(
-            genesis::GenesisConfigBuilder::default()
-                .members(initial_members.to_vec())
-                .build(),
-        )
-        .build()
-        .execute_with(|| {
-            let member_id = 0;
-
-            assert_ok!(Membership::set_controller_account(
-                Origin::signed(ALICE_ACCOUNT_ID),
-                member_id,
-                ALICE_CONTROLLER_ID
-            ));
-
-            let profile = get_membership_by_id(member_id);
-
-            assert_eq!(profile.controller_account, ALICE_CONTROLLER_ID);
-            assert_eq!(
-                <crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_CONTROLLER_ID),
-                vec![member_id]
-            );
-            assert!(
-                <crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty()
-            );
-        });
+fn update_profile_has_no_effect_on_empty_parameters() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get();
+        set_alice_free_balance(initial_balance);
+
+        let next_member_id = Membership::members_created();
+
+        assert_ok!(buy_default_membership_as_alice());
+        assert_ok!(Membership::update_profile(
+            Origin::signed(ALICE_ACCOUNT_ID),
+            next_member_id,
+            None,
+            None,
+            None,
+            None,
+        ));
+
+        let profile = get_membership_by_id(next_member_id);
+
+        assert_eq!(Some(profile.name), get_alice_info().name);
+        assert_eq!(Some(profile.handle), get_alice_info().handle);
+        assert_eq!(Some(profile.avatar_uri), get_alice_info().avatar_uri);
+        assert_eq!(Some(profile.about), get_alice_info().about);
+
+        assert!(<crate::MemberIdByHandle<Test>>::contains_key(
+            get_alice_info().handle.unwrap()
+        ));
+    });
 }
 
 #[test]
-fn set_root_account() {
-    let initial_members = [(0, ALICE_ACCOUNT_ID)];
-    const ALICE_NEW_ROOT_ACCOUNT: u64 = 2;
-
-    TestExternalitiesBuilder::<Test>::default()
-        .set_membership_config(
-            genesis::GenesisConfigBuilder::default()
-                .members(initial_members.to_vec())
-                .build(),
-        )
-        .build()
-        .execute_with(|| {
-            let member_id = 0;
-
-            assert_ok!(Membership::set_root_account(
-                Origin::signed(ALICE_ACCOUNT_ID),
-                member_id,
-                ALICE_NEW_ROOT_ACCOUNT
-            ));
-
-            let membership = Membership::membership(member_id);
-
-            assert_eq!(ALICE_NEW_ROOT_ACCOUNT, membership.root_account);
-
-            assert!(<crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
-        });
+fn update_profile_accounts_succeeds() {
+    let member_id = 0u64;
+    let initial_members = [(member_id, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        const ALICE_NEW_ACCOUNT_ID: u64 = 2;
+
+        assert_ok!(Membership::update_accounts(
+            Origin::signed(ALICE_ACCOUNT_ID),
+            member_id,
+            Some(ALICE_NEW_ACCOUNT_ID),
+            Some(ALICE_NEW_ACCOUNT_ID),
+        ));
+
+        let profile = get_membership_by_id(member_id);
+
+        assert_eq!(profile.controller_account, ALICE_NEW_ACCOUNT_ID);
+        assert_eq!(
+            <crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_NEW_ACCOUNT_ID),
+            vec![member_id]
+        );
+        assert!(<crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
+
+        assert_eq!(profile.root_account, ALICE_NEW_ACCOUNT_ID);
+        assert_eq!(
+            <crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_NEW_ACCOUNT_ID),
+            vec![member_id]
+        );
+        assert!(<crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID).is_empty());
+
+        EventFixture::assert_last_crate_event(Event::<Test>::MemberAccountsUpdated(member_id));
+    });
+}
+
+#[test]
+fn update_accounts_has_effect_on_empty_account_parameters() {
+    let member_id = 0u64;
+    let initial_members = [(member_id, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        assert_ok!(Membership::update_accounts(
+            Origin::signed(ALICE_ACCOUNT_ID),
+            member_id,
+            None,
+            None,
+        ));
+
+        let profile = get_membership_by_id(member_id);
+
+        assert_eq!(profile.controller_account, ALICE_ACCOUNT_ID);
+        assert_eq!(
+            <crate::MemberIdsByControllerAccountId<Test>>::get(&ALICE_ACCOUNT_ID),
+            vec![member_id]
+        );
+
+        assert_eq!(profile.root_account, ALICE_ACCOUNT_ID);
+        assert_eq!(
+            <crate::MemberIdsByRootAccountId<Test>>::get(&ALICE_ACCOUNT_ID),
+            vec![member_id]
+        );
+    });
 }
 
 #[test]
@@ -283,3 +308,99 @@ fn update_verification_status_fails_with_invalid_worker_id() {
                 .into()));
     });
 }
+
+#[test]
+fn buy_membership_fails_with_invalid_name() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get();
+        set_alice_free_balance(initial_balance);
+
+        let name: [u8; 500] = [1; 500];
+
+        let buy_membership_fixture = BuyMembershipFixture::default().with_name(name.to_vec());
+
+        buy_membership_fixture.call_and_assert(Err(Error::<Test>::NameTooLong.into()));
+    });
+}
+
+#[test]
+fn buy_membership_fails_with_non_member_referrer_id() {
+    build_test_externalities().execute_with(|| {
+        let initial_balance = MembershipFee::get();
+        set_alice_free_balance(initial_balance);
+
+        let invalid_member_id = 111;
+
+        let buy_membership_fixture =
+            BuyMembershipFixture::default().with_referrer_id(invalid_member_id);
+
+        buy_membership_fixture.call_and_assert(Err(Error::<Test>::ReferrerIsNotMember.into()));
+    });
+}
+
+#[test]
+fn buy_membership_with_referral_cut_succeeds() {
+    let member_id = 0u64;
+    let initial_members = [(member_id, ALICE_ACCOUNT_ID)];
+
+    build_test_externalities_with_initial_members(initial_members.to_vec()).execute_with(|| {
+        let initial_balance = 10000;
+        increase_total_balance_issuance_using_account_id(BOB_ACCOUNT_ID, initial_balance);
+
+        let buy_membership_fixture = BuyMembershipFixture::default()
+            .with_handle(b"bobs_handle".to_vec())
+            .with_accounts(BOB_ACCOUNT_ID)
+            .with_origin(RawOrigin::Signed(BOB_ACCOUNT_ID))
+            .with_referrer_id(member_id);
+
+        buy_membership_fixture.call_and_assert(Ok(()));
+
+        let referral_cut = Membership::get_referral_bonus();
+
+        assert_eq!(Balances::usable_balance(&ALICE_ACCOUNT_ID), referral_cut);
+        assert_eq!(
+            Balances::usable_balance(&BOB_ACCOUNT_ID),
+            initial_balance - MembershipFee::get()
+        );
+    });
+}
+
+#[test]
+fn referral_bonus_calculated_successfully() {
+    build_test_externalities().execute_with(|| {
+        // it should take minimum of the referral cut and membership fee
+        let membership_fee = MembershipFee::get();
+        let diff = 10;
+
+        let referral_cut = membership_fee.saturating_sub(diff);
+        <crate::ReferralCut<Test>>::put(referral_cut);
+        assert_eq!(Membership::get_referral_bonus(), referral_cut);
+
+        let referral_cut = membership_fee.saturating_add(diff);
+        <crate::ReferralCut<Test>>::put(referral_cut);
+        assert_eq!(Membership::get_referral_bonus(), membership_fee);
+    });
+}
+
+#[test]
+fn set_referral_cut_succeeds() {
+    build_test_externalities().execute_with(|| {
+        let starting_block = 1;
+        run_to_block(starting_block);
+
+        SetReferralCutFixture::default().call_and_assert(Ok(()));
+
+        EventFixture::assert_last_crate_event(Event::<Test>::ReferralCutUpdated(
+            DEFAULT_REFERRAL_CUT_VALUE,
+        ));
+    });
+}
+
+#[test]
+fn set_referral_fails_with_invalid_origin() {
+    build_test_externalities().execute_with(|| {
+        SetReferralCutFixture::default()
+            .with_origin(RawOrigin::Signed(ALICE_ACCOUNT_ID))
+            .call_and_assert(Err(DispatchError::BadOrigin));
+    });
+}

+ 5 - 4
runtime-modules/proposals/codex/src/lib.rs

@@ -1,5 +1,5 @@
 //! # Proposals codex module
-//! Proposals `codex` module for the Joystream platform. Version 3.
+//! Proposals `codex` module for the Joystream platform.
 //! Component of the proposals system. It contains preset proposal types.
 //!
 //! ## Overview
@@ -42,14 +42,15 @@
 //! - [governance](../substrate_governance_module/index.html)
 //!
 //! ### Notes
-//! The module uses [ProposalEncoder](./trait.ProposalEncoder.html) to encode the proposal using
-//! its details. Encoded byte vector is passed to the _proposals engine_ as serialized executable code.
+//! The module uses [ProposalEncoder](./trait.ProposalEncoder.html) to encode the proposal using its
+//! details. Encoded byte vector is passed to the _proposals engine_ as serialized executable code.
 
 // `decl_module!` does a lot of recursion and requires us to increase the limit to 256.
 #![recursion_limit = "256"]
 // Ensure we're `no_std` when compiling for Wasm.
 #![cfg_attr(not(feature = "std"), no_std)]
-// Disable this lint warning because Substrate generates function without an alias for the ProposalDetailsOf type.
+// Disable this lint warning because Substrate generates function without an alias for
+// the ProposalDetailsOf type.
 #![allow(clippy::too_many_arguments)]
 
 mod types;

+ 11 - 7
runtime-modules/proposals/discussion/src/benchmarking.rs

@@ -60,13 +60,17 @@ fn member_account<T: common::Trait + balances::Trait + membership::Trait>(
         "Balance not added",
     );
 
-    Membership::<T>::buy_membership(
-        RawOrigin::Signed(account_id.clone()).into(),
-        Some(handle),
-        None,
-        None,
-    )
-    .unwrap();
+    let params = membership::BuyMembershipParameters {
+        root_account: account_id.clone(),
+        controller_account: account_id.clone(),
+        name: None,
+        handle: Some(handle),
+        avatar_uri: None,
+        about: None,
+        referrer_id: None,
+    };
+
+    Membership::<T>::buy_membership(RawOrigin::Signed(account_id.clone()).into(), params).unwrap();
 
     (account_id, T::MemberId::from(id.try_into().unwrap()))
 }

+ 8 - 6
runtime-modules/proposals/discussion/src/lib.rs

@@ -1,21 +1,23 @@
 //! # Proposals discussion module
-//! Proposals `discussion` module for the Joystream platform. Version 3.
+//! Proposals `discussion` module for the Joystream platform.
 //! It contains discussion subsystem of the proposals.
 //!
 //! ## Overview
 //!
-//! The proposals discussion module is used by the codex module to provide a platform for discussions
-//! about different proposals. It allows to create discussion threads and then add and update related
-//! posts.
+//! The proposals discussion module is used by the codex module to provide a platform for
+//! discussions about different proposals. It allows to create discussion threads and then add and
+//! update related posts.
 //!
 //! ## Supported extrinsics
 //! - [add_post](./struct.Module.html#method.add_post) - adds a post to an existing discussion thread
 //! - [update_post](./struct.Module.html#method.update_post) - updates existing post
-//! - [change_thread_mode](./struct.Module.html#method.change_thread_mode) - changes thread permission mode
+//! - [change_thread_mode](./struct.Module.html#method.change_thread_mode) - changes thread
+//! permission mode
 //!
 //! ## Public API methods
 //! - [create_thread](./struct.Module.html#method.create_thread) - creates a discussion thread
-//! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures safe thread creation
+//! - [ensure_can_create_thread](./struct.Module.html#method.ensure_can_create_thread) - ensures
+//! safe thread creation
 //!
 //! ## Usage
 //!

+ 11 - 7
runtime-modules/proposals/engine/src/benchmarking.rs

@@ -74,13 +74,17 @@ fn member_funded_account<T: Trait + membership::Trait>(
     // Give balance for buying membership
     let _ = Balances::<T>::make_free_balance_be(&account_id, T::Balance::max_value());
 
-    Membership::<T>::buy_membership(
-        RawOrigin::Signed(account_id.clone()).into(),
-        Some(handle),
-        None,
-        None,
-    )
-    .unwrap();
+    let params = membership::BuyMembershipParameters {
+        root_account: account_id.clone(),
+        controller_account: account_id.clone(),
+        name: None,
+        handle: Some(handle),
+        avatar_uri: None,
+        about: None,
+        referrer_id: None,
+    };
+
+    Membership::<T>::buy_membership(RawOrigin::Signed(account_id.clone()).into(), params).unwrap();
 
     let _ = Balances::<T>::make_free_balance_be(&account_id, T::Balance::max_value());
 

+ 26 - 17
runtime-modules/proposals/engine/src/lib.rs

@@ -1,22 +1,25 @@
 //! # Proposals engine module
-//! Proposals `engine` module for the Joystream platform. Version 3.
+//! Proposals `engine` module for the Joystream platform.
 //! The main component of the proposals system. Provides methods and extrinsics to create and
 //! vote for proposals, inspired by Parity **Democracy module**.
 //!
 //! ## Overview
-//! Proposals `engine` module provides an abstract mechanism to work with proposals: creation, voting,
-//! execution, canceling, etc. Proposal execution demands serialized _Dispatchable_ proposal code.
-//! It could be any _Dispatchable_ + _Parameter_ type, but most likely, it would be serialized (via
-//! Parity _codec_ crate) extrisic call. A proposal stage can be described by its [status](./enum.ProposalStatus.html).
+//! Proposals `engine` module provides an abstract mechanism to work with proposals: creation,
+//! voting, execution, canceling, etc. Proposal execution demands serialized _Dispatchable_ proposal
+//! code. It could be any _Dispatchable_ + _Parameter_ type, but most likely, it would be serialized
+//! (via Parity _codec_ crate) extrisic call. A proposal stage can be described by
+//! its [status](./enum.ProposalStatus.html).
 //!
 //! ## Proposal lifecycle
 //! When a proposal passes [checks](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid)
-//! for its [parameters](./struct.ProposalParameters.html) - it can be [created](./struct.Module.html#method.create_proposal).
-//! The newly created proposal has _Active_ status. The proposal can be voted on, vetoed or canceled during its
-//! _voting period_. Votes can be [different](./enum.VoteKind.html). When the proposal gets enough votes
-//! to be approved - the proposal becomes _PendingExecution_ or _PendingConstitutionality_. The proposal
-//! could also be slashed or rejected. If the _voting period_ ends with no decision it becomes expired.
-//! If the proposal got approved and _grace period_ passed - the  `engine` module tries to execute the proposal.
+//! for its [parameters](./struct.ProposalParameters.html) -
+//! it can be [created](./struct.Module.html#method.create_proposal).
+//! The newly created proposal has _Active_ status. The proposal can be voted on, vetoed or
+//! canceled during its _voting period_. Votes can be [different](./enum.VoteKind.html). When the
+//! proposal gets enough votes to be approved - the proposal becomes _PendingExecution_ or
+//! _PendingConstitutionality_. The proposal could also be slashed or rejected. If the _voting
+//! period_ ends with no decision it becomes expired. If the proposal got approved
+//! and _grace period_ passed - the  `engine` module tries to execute the proposal.
 //!
 //! ### Notes
 //!
@@ -24,7 +27,8 @@
 //! anytime before the proposal execution by the _sudo_.
 //! - If the _council_ got reelected during the proposal _voting period_ the external handler calls
 //! [reject_active_proposals](./trait.Module.html#method.reject_active_proposals) function and
-//! all active proposals got rejected and it also calls [reactivate_pending_constitutionality_proposals](./trait.Module.html#method.reactivate_pending_constitutionality_proposals)
+//! all active proposals got rejected and it also calls
+//! [reactivate_pending_constitutionality_proposals](./trait.Module.html#method.reactivate_pending_constitutionality_proposals)
 //! and proposals with pending constitutionality become active again.
 //! - There are different fees to apply for slashed, rejected, expired or cancelled proposals.
 //!
@@ -42,14 +46,19 @@
 //!
 //! ### Supported extrinsics
 //! - [vote](./struct.Module.html#method.vote) - registers a vote for the proposal
-//! - [cancel_proposal](./struct.Module.html#method.cancel_proposal) - cancels the proposal (can be canceled only by owner)
+//! - [cancel_proposal](./struct.Module.html#method.cancel_proposal) - cancels the proposal
+//! (can be canceled only by owner)
 //! - [veto_proposal](./struct.Module.html#method.veto_proposal) - vetoes the proposal
 //!
 //! ### Public API
-//! - [create_proposal](./struct.Module.html#method.create_proposal) - creates proposal using provided parameters
-//! - [ensure_create_proposal_parameters_are_valid](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid) - ensures that we can create the proposal
-//! - [reject_active_proposals](./trait.Module.html#method.reject_active_proposals) - rejects all active proposals.
-//! - [reactivate_pending_constitutionality_proposals](./trait.Module.html#method.reactivate_pending_constitutionality_proposals) - reactivate proposals with pending constitutionality.
+//! - [create_proposal](./struct.Module.html#method.create_proposal) - creates proposal using
+//! provided parameters
+//! - [ensure_create_proposal_parameters_are_valid](./struct.Module.html#method.ensure_create_proposal_parameters_are_valid)
+//! - ensures that we can create the proposal
+//! - [reject_active_proposals](./trait.Module.html#method.reject_active_proposals) - rejects all
+//! active proposals.
+//! - [reactivate_pending_constitutionality_proposals](./trait.Module.html#method.reactivate_pending_constitutionality_proposals)
+//! - reactivate proposals with pending constitutionality.
 //!
 //! ## Usage
 //!

+ 1 - 1
runtime-modules/storage/src/tests/mock.rs

@@ -366,7 +366,7 @@ impl ExtBuilder {
                 handle: "alice".into(),
                 avatar_uri: "".into(),
                 about: "".into(),
-                registered_at_time: 0,
+                name: "".into(),
             }],
         }
         .assimilate_storage(&mut t)

+ 11 - 7
runtime-modules/working-group/src/benchmarking.rs

@@ -181,13 +181,17 @@ fn member_funded_account<T: Trait<I> + membership::Trait, I: Instance>(
 
     let _ = Balances::<T>::make_free_balance_be(&account_id, BalanceOf::<T>::max_value());
 
-    Membership::<T>::buy_membership(
-        RawOrigin::Signed(account_id.clone()).into(),
-        Some(handle),
-        None,
-        None,
-    )
-    .unwrap();
+    let params = membership::BuyMembershipParameters {
+        root_account: account_id.clone(),
+        controller_account: account_id.clone(),
+        name: None,
+        handle: Some(handle),
+        avatar_uri: None,
+        about: None,
+        referrer_id: None,
+    };
+
+    Membership::<T>::buy_membership(RawOrigin::Signed(account_id.clone()).into(), params).unwrap();
 
     let _ = Balances::<T>::make_free_balance_be(&account_id, BalanceOf::<T>::max_value());
 

+ 12 - 7
runtime/src/tests/mod.rs

@@ -34,13 +34,18 @@ pub(crate) fn insert_member(account_id: AccountId32) {
         crate::MembershipFee::get(),
     );
     let handle: &[u8] = account_id.as_ref();
-    Membership::buy_membership(
-        RawOrigin::Signed(account_id.clone()).into(),
-        Some(handle.to_vec()),
-        None,
-        None,
-    )
-    .unwrap();
+
+    let params = membership::BuyMembershipParameters {
+        root_account: account_id.clone(),
+        controller_account: account_id.clone(),
+        name: None,
+        handle: Some(handle.to_vec()),
+        avatar_uri: None,
+        about: None,
+        referrer_id: None,
+    };
+
+    Membership::buy_membership(RawOrigin::Signed(account_id.clone()).into(), params).unwrap();
 }
 
 pub(crate) fn increase_total_balance_issuance_using_account_id(