//! # Teams & Advisors Unit Pallet
//!
//! Pallet that allows token owners to manage team members and advisors
//!
//! ## Overview
//!
//! TeamAdvisors module provides functionality to the token owners to
//! add, remove, and edit team members and advisors.
//!
//! The supported dispatchable functions are documented in the [`Call`] enum.
//!
//! ### Goals
//!
//! The pallet is designed to make the following possible:
//!
//! * Add new members by the asset´s owner.
//! * Remove members by the asset´s owner.
//! * Edit member info by the asset´s owner.
//! * Add advisors by the asset´s owner.
//! * Remove advisors by the asset´s owner.
//!
//! ## Interface
//!
//! ### Privileged Functions
//!
//! * `add_member`: Add a member for the given asset, called by the asset´s Owner.
//! * `update_member`: Update the info of an existing member, called by the asset´s Owner.
//! * `delete_member`: Remove the given member for the given asset, called by the asset´s Owner.
//! * `create_advisor`: Add an advisor for the given asset, called by the asset´s Owner.
//! * `remove_advisor`: Remove the given advisor for the given asset, called by the asset´s Owner.
//!
//! Please refer to the [`Call`] enum and its associated variants for documentation on each
//! function.
#![cfg_attr(not(feature = "std"), no_std)]
/// Edit this file to define custom logic or remove it if it is not needed.
/// Learn more about FRAME and the core library of Substrate FRAME pallets:
/// <https://docs.substrate.io/reference/frame-pallets/>
pub use pallet::*;
use frame_support::{ traits::{ fungibles, Time } };
use sp_runtime::traits::Zero;
use frame_support::traits::tokens::Balance;
use sp_runtime::FixedPointOperand;
use sp_std::vec;
use traits::{
teamsadvisors::{ TeamsAdvisorsInspect, TeamsAdvisorsBenchmark },
profile::ProfileInspect,
asset::{AssetInterface, TokenType},
subaccounts::{ SubAccounts, AccountOrigin },
vesting::VestingInterface,
};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use weights::WeightInfo;
#[frame_support::pallet]
pub mod pallet {
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use frame_support::pallet_prelude::Member as MemberTrait;
use super::*;
/// Asset id type alias.
pub type AssetIdOf<T> = <T as Config>::AssetId;
pub type BalanceOf<T> = <T as Config>::AssetBalance;
pub type MomentOf<T> = <<T as Config>::Time as Time>::Moment;
pub type Decimals = u8;
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
/// Configure the pallet by specifying the parameters and types on which it depends.
#[pallet::config]
pub trait Config: frame_system::Config {
/// Because this pallet emits events, it depends on the runtime's definition of an event.
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type AssetId: MemberTrait +
Parameter +
Copy +
MaybeSerializeDeserialize +
MaxEncodedLen +
Default +
Zero +
From<u32>;
// Type to access the Assets Pallet.
type Fungibles: fungibles::Inspect<
Self::AccountId,
AssetId = Self::AssetId,
Balance = Self::AssetBalance
> +
AssetInterface<AssetIdOf<Self>, BalanceOf<Self>, Self::AccountId, Decimals, TokenType>;
type AssetBalance: Balance +
FixedPointOperand +
MaxEncodedLen +
MaybeSerializeDeserialize +
TypeInfo;
/// Type to access the profile pallet
type Profile: ProfileInspect<Self::AccountId, Self::ProfileStringLimit>;
/// Time provider
type Time: Time;
/// The maximum length of strings.
#[pallet::constant]
type ProfileStringLimit: Get<u32>;
/// Type to access the sub account pallet
type SubAccounts: SubAccounts<Self::AccountId, AccountOrigin>;
/// Type to access the vesting pallet
type Vesting: VestingInterface<
Self::AccountId,
Self::AssetId,
Self::AssetBalance,
BlockNumberFor<Self>
>;
/// Helper trait for benchmarks.
#[cfg(feature = "runtime-benchmarks")]
type BenchmarkHelper;
#[cfg(feature = "runtime-benchmarks")]
type BankBenchmark;
// Weight information for extrinsics in this pallet.
type WeightInfo: WeightInfo;
}
/// Data related to a team member
#[derive(CloneNoBound, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Member<Balance: Clone, BlockNumber: Clone, AccountId: Clone, Moment: Clone> {
/// Token quantity assigned to the member.
pub token_quantity: Balance,
/// Cliff period of the tokens allocated. (Read only)
pub cliff_period: BlockNumber,
/// Vesting period of the tokens allocated. (Read Only)
pub vest_period: BlockNumber,
/// Account Id of the member.
pub account: AccountId,
/// Date when the member was added
pub joined_at: Moment,
}
/// Data related to an advisor
#[derive(CloneNoBound, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo)]
pub struct Advisor<AccountId: Clone, Moment: Clone> {
/// Account Id of the advisor.
pub account: AccountId,
/// Date when the advisor was added
pub joined_at: Moment,
}
#[pallet::storage]
/// Member info of a specific account for a specific asset.
#[pallet::getter(fn members)]
pub type AllMembers<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
T::AccountId,
Member<BalanceOf<T>, BlockNumberFor<T>, T::AccountId, MomentOf<T>>
>;
#[pallet::storage]
/// Advisor info of a specific account for a specific asset.
#[pallet::getter(fn advisors)]
pub type Advisors<T: Config> = StorageDoubleMap<
_,
Blake2_128Concat,
AssetIdOf<T>,
Blake2_128Concat,
T::AccountId,
Advisor<T::AccountId, MomentOf<T>>
>;
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
/// A member was added.
MemberAdded {
asset_id: AssetIdOf<T>,
token_quantity: BalanceOf<T>,
cliff_period: BlockNumberFor<T>,
vest_period: BlockNumberFor<T>,
account: T::AccountId,
},
/// Member info updated
MemberUpdated {
asset_id: AssetIdOf<T>,
account: T::AccountId,
new_token_quantity: BalanceOf<T>,
new_cliff_period: BlockNumberFor<T>,
new_vest_period: BlockNumberFor<T>,
},
/// A Member was deleted
MemberDeleted {
asset_id: AssetIdOf<T>,
account: T::AccountId,
},
/// Advisor created
AdvisorCreated {
asset_id: AssetIdOf<T>,
account: T::AccountId,
},
/// An advisor was deleted
AdvisorDeleted {
asset_id: AssetIdOf<T>,
user_address: T::AccountId,
},
/// Advisor info was updated
AdvisorUpdated {
asset_id: AssetIdOf<T>,
user_address: T::AccountId,
joined_at: MomentOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
/// Only the token owner can call this functions.
NotOwner,
/// Invalid asset id.
InvalidAsset,
/// The user is already a member.
AlreadyMember,
/// The member is not registered.
MemberDoesNotExist,
/// The user is already an advisor
AdvisorAlreadyExists,
/// The advisor is not registered
AdvisorDoesNotExist,
}
#[pallet::call(weight(<T as Config>::WeightInfo))]
impl<T: Config> Pallet<T> {
/// Add a member for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// If the token quantity is greater than 0 this function call the vesting pallet to allocate tokens.
///
/// - `asset_id`: The identifier of the asset to add the member.
/// - `member_address`: The account to be added as a member.
/// - `token_quantity`: The amount of tokens that are allocated.
/// - `cliff_period`: The cliff period for the allocated amount, given in amount of blocks.
/// - `vest_period`: The vesting period for the allocated amount, given in amount of blocks.
///
/// Emits `MemberAdded` event when successful.
///
#[pallet::call_index(0)]
pub fn add_member(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
member_address: T::AccountId,
token_quantity: BalanceOf<T>,
cliff_period: BlockNumberFor<T>,
vest_period: BlockNumberFor<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account of the sender to store info
who = T::SubAccounts::get_main_account(who)?;
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id)?;
// Check if the address has a profile
T::Profile::get_profile_address(member_address.clone())?;
ensure!(
!AllMembers::<T>::contains_key(asset_id, member_address.clone()),
Error::<T>::AlreadyMember
);
ensure!(
!Advisors::<T>::contains_key(asset_id, member_address.clone()),
Error::<T>::AdvisorAlreadyExists
);
// Create a vesting schedule if there are tokens allocated
if !token_quantity.is_zero() {
T::Vesting::create_vesting(
member_address.clone(),
asset_id.clone(),
token_quantity.clone(),
cliff_period.clone(),
vest_period.clone()
)?;
}
let now = T::Time::now();
let new_member = Member {
token_quantity,
cliff_period,
vest_period,
account: member_address.clone(),
joined_at: now,
};
AllMembers::<T>::insert(asset_id, member_address.clone(), new_member);
Self::deposit_event(Event::MemberAdded {
asset_id,
token_quantity,
cliff_period,
vest_period,
account: member_address,
});
// Return a successful DispatchResultWithPostInfo
Ok(())
}
/// Add an existing member for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// The account should be already registered as a member.
///
/// This functions call the vesting pallet to edit the vesting information for the user.
///
/// - `asset_id`: The identifier of the asset to edit the member.
/// - `member_address`: The address of the member.
/// - `new_token_quantity`: The new amount of assets that are allocated.
/// - `new_cliff_period`: The new cliff period for the allocated amount, given in amount of blocks.
/// - `new_vest_period`: The new vesting period for the allocated amount, given in amount of blocks.
/// - `joined_at`: The moment where this user joined the team.
///
/// Emits `MemberUpdated` event when successful.
///
#[pallet::call_index(1)]
pub fn update_member(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
member_address: T::AccountId,
new_token_quantity: BalanceOf<T>,
new_cliff_period: BlockNumberFor<T>,
new_vest_period: BlockNumberFor<T>,
joined_at: MomentOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account of the sender
who = T::SubAccounts::get_main_account(who)?;
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id)?;
// Check if the address has a profile
T::Profile::get_profile_address(member_address.clone())?;
let member = AllMembers::<T>
::get(asset_id, &member_address)
.ok_or(Error::<T>::MemberDoesNotExist)?;
// Update vesting schedule if there is at least one field to update
if
!new_token_quantity.is_zero() ||
member.cliff_period != new_cliff_period ||
member.vest_period != new_vest_period
{
T::Vesting::update_vesting(
member_address.clone(),
asset_id,
new_token_quantity,
new_cliff_period,
new_vest_period
)?;
}
AllMembers::<T>::mutate(asset_id, &member_address, |member| {
member.as_mut().unwrap().token_quantity = new_token_quantity;
member.as_mut().unwrap().cliff_period = new_cliff_period;
member.as_mut().unwrap().vest_period = new_vest_period;
member.as_mut().unwrap().joined_at = joined_at;
});
Self::deposit_event(Event::MemberUpdated {
asset_id,
account: member_address,
new_token_quantity,
new_cliff_period,
new_vest_period,
});
return Ok(());
}
/// Remove an existing member for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// - `asset_id`: The identifier of the asset to remove the member.
/// - `member_address`: The username of the account to remove.
///
/// Emits `MemberDeleted` event when successful.
///
#[pallet::call_index(2)]
pub fn delete_member(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
member_address: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account of the sender
who = T::SubAccounts::get_main_account(who)?;
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id)?;
// Check if the address has a profile
T::Profile::get_profile_address(member_address.clone())?;
ensure!(
AllMembers::<T>::contains_key(asset_id, &member_address),
Error::<T>::MemberDoesNotExist
);
T::Vesting::delete_vesting(asset_id, member_address.clone())?;
AllMembers::<T>::remove(asset_id, &member_address);
Self::deposit_event(Event::MemberDeleted {
asset_id,
account: member_address,
});
return Ok(());
}
/// Create an advisor for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// - `asset_id`: The identifier of the asset to add the advisor.
/// - `advisor_address`: The account to add.
///
/// Emits `AdvisorCreated` event when successful.
///
#[pallet::call_index(3)]
pub fn create_advisor(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
advisor_address: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account of the sender
who = T::SubAccounts::get_main_account(who)?;
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id)?;
T::Profile::get_profile_address(advisor_address.clone())?;
ensure!(
!Advisors::<T>::contains_key(asset_id, &advisor_address),
Error::<T>::AdvisorAlreadyExists
);
ensure!(
!AllMembers::<T>::contains_key(asset_id, &advisor_address),
Error::<T>::AlreadyMember
);
let now = T::Time::now();
let new_advisor = Advisor {
account: advisor_address.clone(),
joined_at: now,
};
Advisors::<T>::insert(asset_id, &advisor_address, new_advisor);
Self::deposit_event(Event::AdvisorCreated {
asset_id,
account: advisor_address,
});
return Ok(());
}
/// Remove an advisor for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// - `asset_id`: The identifier of the asset to remove the advisor.
/// - `advisor_address`: The account to remove.
///
/// Emits `AdvisorDeleted` event when successful.
///
#[pallet::call_index(4)]
pub fn remove_advisor(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
advisor_address: T::AccountId
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account of the sender
who = T::SubAccounts::get_main_account(who)?;
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id)?;
T::Profile::get_profile_address(advisor_address.clone())?;
ensure!(
Advisors::<T>::contains_key(asset_id, &advisor_address),
Error::<T>::AdvisorDoesNotExist
);
Advisors::<T>::remove(asset_id, &advisor_address);
Self::deposit_event(Event::AdvisorDeleted {
asset_id,
user_address: advisor_address,
});
return Ok(());
}
/// Update an advisor's info for a particular asset.
///
/// The origin must be Signed and the sender must be the Owner of the asset `asset_id`.
///
/// The address should exist in the profile pallet and be an advisor.
///
/// - `asset_id`: The identifier of the asset to remove the advisor.
/// - `advisor_address`: The account to edit.
/// - `joined_at`: The moment where the user joined as an advisor.
///
/// Emits `AdvisorDeleted` event when successful.
///
#[pallet::call_index(5)]
pub fn update_advisor(
origin: OriginFor<T>,
asset_id: AssetIdOf<T>,
advisor_address: T::AccountId,
joined_at: MomentOf<T>
) -> DispatchResult {
let mut who = ensure_signed(origin)?;
// Mutate the origin to use the main account of the sender
who = T::SubAccounts::get_main_account(who)?;
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id)?;
// Check if the address has a profile
T::Profile::get_profile_address(advisor_address.clone())?;
ensure!(
Advisors::<T>::contains_key(asset_id, &advisor_address),
Error::<T>::AdvisorDoesNotExist
);
Advisors::<T>::try_mutate(
&asset_id,
&advisor_address,
|advisor| -> Result<(), DispatchError> {
let advisor = advisor.as_mut().ok_or(Error::<T>::AdvisorDoesNotExist)?;
advisor.joined_at = joined_at.clone();
Ok(())
}
)?;
Self::deposit_event(Event::AdvisorUpdated {
asset_id,
user_address: advisor_address,
joined_at,
});
return Ok(());
}
}
impl<T: Config> Pallet<T> {
/// Check if the sender is owner of the asset.
fn check_permission(who: T::AccountId, asset_id: AssetIdOf<T>) -> DispatchResult {
// check if the sender has permission to add members
let asset_owner = T::Fungibles::get_owner(asset_id)?;
ensure!(who == asset_owner, Error::<T>::NotOwner);
Ok(())
}
}
/// Implements [`TeamsAdvisorsInspect`] trait for pallets to check if an account ID is member or advisor .
impl<T: Config> TeamsAdvisorsInspect<AssetIdOf<T>, T::AccountId> for Pallet<T> {
fn is_member(asset_id: AssetIdOf<T>, who: T::AccountId) -> bool {
AllMembers::<T>::contains_key(asset_id, who)
}
fn is_advisor(asset_id: AssetIdOf<T>, who: T::AccountId) -> bool {
Advisors::<T>::contains_key(asset_id, who)
}
}
impl<T: Config> TeamsAdvisorsBenchmark<
AssetIdOf<T>,
T::AccountId,
BalanceOf<T>,
BlockNumberFor<T>,
T::ProfileStringLimit
>
for Pallet<T> {
fn add_member(
who: T::AccountId,
asset_id: AssetIdOf<T>,
member_address: T::AccountId,
token_quantity: BalanceOf<T>,
cliff_period: BlockNumberFor<T>,
vest_period: BlockNumberFor<T>
) -> DispatchResult {
// check if the sender has permission to call this extrinsic
Self::check_permission(who.clone(), asset_id.clone())?;
// Check if the address has a profile
T::Profile::get_profile_address(member_address.clone())?;
ensure!(
!AllMembers::<T>::contains_key(&asset_id, member_address.clone()),
Error::<T>::AlreadyMember
);
ensure!(
!Advisors::<T>::contains_key(&asset_id, member_address.clone()),
Error::<T>::AdvisorAlreadyExists
);
// Create a vesting schedule if there are tokens allocated
if !token_quantity.is_zero() {
T::Vesting::create_vesting(
member_address.clone(),
asset_id.clone(),
token_quantity.clone(),
cliff_period.clone(),
vest_period.clone()
)?;
}
let now = T::Time::now();
let new_member = Member {
token_quantity: token_quantity.clone(),
cliff_period: cliff_period.clone(),
vest_period: vest_period.clone(),
account: member_address.clone(),
joined_at: now,
};
AllMembers::<T>::insert(&asset_id, member_address.clone(), new_member);
Self::deposit_event(Event::MemberAdded {
asset_id: asset_id,
token_quantity: token_quantity,
cliff_period: cliff_period,
vest_period: vest_period,
account: member_address,
});
// Return a successful DispatchResultWithPostInfo
Ok(())
}
}
}