diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs
index e465d58fb..cb6d23b2f 100644
--- a/pallets/roles/src/impls.rs
+++ b/pallets/roles/src/impls.rs
@@ -15,7 +15,7 @@
// along with Tangle. If not, see .
use super::*;
-use sp_runtime::Saturating;
+use sp_runtime::{DispatchResult, Percent, Saturating};
use tangle_primitives::{roles::RoleType, traits::roles::RolesHandler};
/// Implements RolesHandler for the pallet.
@@ -29,16 +29,7 @@ impl RolesHandler for Pallet {
/// # Returns
/// Returns `true` if the validator is permitted to work with this job type, otherwise `false`.
fn is_validator(address: T::AccountId, role: RoleType) -> bool {
- let assigned_role = AccountRolesMapping::::get(address);
- match assigned_role {
- Some(r) =>
- if r == role {
- return true
- },
- None => return false,
- }
-
- false
+ Self::has_role(address, role)
}
/// Slash validator stake for the reported offence. The function should be a best effort
@@ -63,6 +54,84 @@ impl RolesHandler for Pallet {
/// Functions for the pallet.
impl Pallet {
+ /// Add new role to the given account.
+ ///
+ /// # Parameters
+ /// - `account`: The account ID of the validator.
+ /// - `role`: Selected role type.
+ pub fn add_role(account: T::AccountId, role: RoleType) -> DispatchResult {
+ AccountRolesMapping::::try_mutate(&account, |roles| {
+ if !roles.contains(&role) {
+ roles.try_push(role.clone()).map_err(|_| Error::::MaxRoles)?;
+
+ Ok(())
+ } else {
+ Err(Error::::HasRoleAssigned.into())
+ }
+ })
+ }
+
+ /// Remove role from the given account.
+ ///
+ /// # Parameters
+ /// - `account`: The account ID of the validator.
+ /// - `role`: Selected role type.
+ pub fn remove_role(account: T::AccountId, role: RoleType) -> DispatchResult {
+ AccountRolesMapping::::try_mutate(&account, |roles| {
+ if roles.contains(&role) {
+ roles.retain(|r| r != &role);
+
+ Ok(())
+ } else {
+ Err(Error::::NoRoleAssigned.into())
+ }
+ })
+ }
+
+ /// Check if the given account has the given role.
+ ///
+ /// # Parameters
+ /// - `account`: The account ID of the validator.
+ /// - `role`: Selected role type.
+ ///
+ /// # Returns
+ /// Returns `true` if the validator is permitted to work with this job type, otherwise `false`.
+ pub fn has_role(account: T::AccountId, role: RoleType) -> bool {
+ let assigned_roles = AccountRolesMapping::::get(account);
+ match assigned_roles.iter().find(|r| **r == role) {
+ Some(_) => true,
+ None => false,
+ }
+ }
+
+ /// Check if account can chill, unbound and withdraw funds.
+ ///
+ /// # Parameters
+ /// - `account`: The account ID of the validator.
+ ///
+ /// # Returns
+ /// Returns boolean value.
+ pub fn can_exit(account: T::AccountId) -> bool {
+ let assigned_roles = AccountRolesMapping::::get(account);
+ if assigned_roles.is_empty() {
+ // Role is cleared, account can chill, unbound and withdraw funds.
+ return true
+ }
+ false
+ }
+
+ /// Calculate max re-stake amount for the given account.
+ ///
+ /// # Parameters
+ /// - `total_stake`: Total stake of the validator
+ ///
+ /// # Returns
+ /// Returns the max re-stake amount.
+ pub fn calculate_max_re_stake_amount(total_stake: BalanceOf) -> BalanceOf {
+ // User can re-stake max 50% of the total stake
+ Percent::from_percent(50) * total_stake
+ }
+
/// Get the total amount of the balance that is locked for the given stash.
///
/// # Parameters
diff --git a/pallets/roles/src/lib.rs b/pallets/roles/src/lib.rs
index ad643a145..d33d35c60 100644
--- a/pallets/roles/src/lib.rs
+++ b/pallets/roles/src/lib.rs
@@ -22,18 +22,15 @@ use codec::MaxEncodedLen;
use frame_support::{
ensure,
traits::{Currency, Get},
- CloneNoBound, EqNoBound, PalletId, PartialEqNoBound, RuntimeDebugNoBound,
+ CloneNoBound, EqNoBound, PartialEqNoBound, RuntimeDebugNoBound,
};
pub use pallet::*;
use parity_scale_codec::{Decode, Encode};
use scale_info::TypeInfo;
-use sp_runtime::{codec, traits::Zero};
+use sp_runtime::{codec, traits::Zero, Saturating};
use sp_std::{convert::TryInto, prelude::*, vec};
-use tangle_primitives::{
- roles::{ReStakingOption, RoleType},
- traits::roles::RolesHandler,
-};
+use tangle_primitives::roles::RoleType;
mod impls;
#[cfg(test)]
pub(crate) mod mock;
@@ -57,16 +54,29 @@ pub use weights::WeightInfo;
pub struct RoleStakingLedger {
/// The stash account whose balance is actually locked and at stake.
pub stash: T::AccountId,
- /// The total amount of the stash's balance that is re-staked for selected services
+ /// The total amount of the stash's balance that is re-staked for all selected roles.
/// This re-staked balance we are currently accounting for new slashing conditions.
#[codec(compact)]
pub total: BalanceOf,
+ /// The list of roles and their re-staked amounts.
+ pub roles: Vec>,
+}
+
+/// The information regarding the re-staked amount for a particular role.
+#[derive(PartialEqNoBound, EqNoBound, Encode, Decode, RuntimeDebugNoBound, TypeInfo, Clone)]
+#[scale_info(skip_type_params(T))]
+pub struct RoleStakingRecord {
+ /// Role type
+ pub role: RoleType,
+ /// The total amount of the stash's balance that is re-staked for selected role.
+ #[codec(compact)]
+ pub re_staked: BalanceOf,
}
impl RoleStakingLedger {
/// Initializes the default object using the given `validator`.
pub fn default_from(stash: T::AccountId) -> Self {
- Self { stash, total: Zero::zero() }
+ Self { stash, total: Zero::zero(), roles: vec![] }
}
/// Returns `true` if the stash account has no funds at all.
@@ -95,10 +105,10 @@ pub mod pallet {
/// The overarching event type.
type RuntimeEvent: From> + IsType<::RuntimeEvent>;
- type WeightInfo: WeightInfo;
-
#[pallet::constant]
- type PalletId: Get;
+ type MaxRolesPerAccount: Get;
+
+ type WeightInfo: WeightInfo;
}
#[pallet::event]
@@ -120,8 +130,10 @@ pub mod pallet {
HasRoleAssigned,
/// No role assigned to provided validator.
NoRoleAssigned,
+ /// Max role limit reached for the account.
+ MaxRoles,
/// Invalid Re-staking amount, should not exceed total staked amount.
- InvalidReStakingBond,
+ ExceedsMaxReStakeValue,
/// Re staking amount should be greater than minimum re-staking bond requirement.
InsufficientReStakingBond,
/// Stash controller account already added to Roles Ledger
@@ -137,22 +149,27 @@ pub mod pallet {
StorageMap<_, Blake2_128Concat, T::AccountId, RoleStakingLedger>;
#[pallet::storage]
- #[pallet::getter(fn account_role)]
+ #[pallet::getter(fn account_roles)]
/// Mapping of resource to bridge index
- pub type AccountRolesMapping = StorageMap<_, Blake2_256, T::AccountId, RoleType>;
+ pub type AccountRolesMapping = StorageMap<
+ _,
+ Blake2_256,
+ T::AccountId,
+ BoundedVec,
+ ValueQuery,
+ >;
/// The minimum re staking bond to become and maintain the role.
#[pallet::storage]
#[pallet::getter(fn min_active_bond)]
pub(super) type MinReStakingBond = StorageValue<_, BalanceOf, ValueQuery>;
- /// Assigns a role to the validator.
+ /// Assigns roles to the validator.
///
/// # Parameters
///
/// - `origin`: Origin of the transaction.
- /// - `role`: Role to assign to the validator.
- /// - `re_stake`: Amount of funds you want to re-stake.
+ /// - `records`: List of roles user is interested to re-stake.
///
/// This function will return error if
/// - Account is not a validator account.
@@ -162,10 +179,9 @@ pub mod pallet {
impl Pallet {
#[pallet::weight({0})]
#[pallet::call_index(0)]
- pub fn assign_role(
+ pub fn assign_roles(
origin: OriginFor,
- role: RoleType,
- re_stake: ReStakingOption,
+ records: Vec>,
) -> DispatchResult {
let stash_account = ensure_signed(origin)?;
// Ensure stash account is a validator.
@@ -174,36 +190,49 @@ pub mod pallet {
Error::::NotValidator
);
- // Check if role is already assigned.
- ensure!(
- !AccountRolesMapping::::contains_key(&stash_account),
- Error::::HasRoleAssigned
- );
-
- // Check if stash account is already paired/ re-staked.
- ensure!(!>::contains_key(&stash_account), Error::::AccountAlreadyPaired);
+ let mut ledger = Ledger::::get(&stash_account)
+ .unwrap_or(RoleStakingLedger::::default_from(stash_account.clone()));
let staking_ledger =
pallet_staking::Ledger::::get(&stash_account).ok_or(Error::::NotValidator)?;
- let re_stake_amount = match re_stake {
- ReStakingOption::Full => staking_ledger.active,
- ReStakingOption::Custom(x) => x.into(),
- };
-
- // Validate re-staking bond, should be greater than min re-staking bond requirement.
- let min_re_staking_bond = MinReStakingBond::::get();
- ensure!(re_stake_amount >= min_re_staking_bond, Error::::InsufficientReStakingBond);
- // Validate re-staking bond, should not exceed active staked bond.
- ensure!(staking_ledger.active >= re_stake_amount, Error::::InvalidReStakingBond);
+ let max_re_staking_bond = Self::calculate_max_re_stake_amount(staking_ledger.active);
+
+ // Validate role staking records.
+ for record in records.clone() {
+ let role = record.role;
+ let re_stake_amount = record.re_staked;
+ // Check if role is already assigned.
+ ensure!(!Self::has_role(stash_account.clone(), role), Error::::HasRoleAssigned);
+
+ // Re-staking amount of record should meet min re-staking amount requirement.
+ let min_re_staking_bond = MinReStakingBond::::get();
+ ensure!(
+ re_stake_amount >= min_re_staking_bond,
+ Error::::InsufficientReStakingBond
+ );
+
+ // Total re_staking amount should not exceed max_re_staking_amount.
+ ensure!(
+ ledger.total.saturating_add(re_stake_amount) <= max_re_staking_bond,
+ Error::::ExceedsMaxReStakeValue
+ );
+
+ ledger.total = ledger.total.saturating_add(re_stake_amount);
+ let role_info = RoleStakingRecord { role, re_staked: re_stake_amount };
+ ledger.roles.push(role_info);
+ }
- // Update ledger.
- let item = RoleStakingLedger { stash: stash_account.clone(), total: re_stake_amount };
- Self::update_ledger(&stash_account, &item);
+ // Now that records are validated we can add them and update ledger
+ for record in records {
+ Self::add_role(stash_account.clone(), record.role)?;
+ Self::deposit_event(Event::::RoleAssigned {
+ account: stash_account.clone(),
+ role: record.role,
+ });
+ }
+ Self::update_ledger(&stash_account, &ledger);
- // Add role mapping for the stash account.
- AccountRolesMapping::::insert(&stash_account, role);
- Self::deposit_event(Event::::RoleAssigned { account: stash_account.clone(), role });
Ok(())
}
@@ -229,16 +258,14 @@ pub mod pallet {
);
// check if role is assigned.
- ensure!(
- Self::is_validator(stash_account.clone(), role.clone()),
- Error::::NoRoleAssigned
- );
+ ensure!(Self::has_role(stash_account.clone(), role), Error::::NoRoleAssigned);
+
// TODO: Call jobs manager to remove the services.
// On successful removal of services, remove the role from the mapping.
// Issue link for reference : https://github.com/webb-tools/tangle/issues/292
// Remove role from the mapping.
- AccountRolesMapping::::remove(&stash_account);
+ Self::remove_role(stash_account.clone(), role)?;
// Remove stash account related info.
Self::kill_stash(&stash_account);
@@ -264,7 +291,7 @@ pub mod pallet {
pub fn chill(origin: OriginFor) -> DispatchResult {
let account = ensure_signed(origin.clone())?;
// Ensure no role is assigned to the account before chilling.
- ensure!(!AccountRolesMapping::::contains_key(&account), Error::::HasRoleAssigned);
+ ensure!(Self::can_exit(account), Error::::HasRoleAssigned);
// chill
pallet_staking::Pallet::::chill(origin)
@@ -292,7 +319,7 @@ pub mod pallet {
) -> DispatchResult {
let account = ensure_signed(origin.clone())?;
// Ensure no role is assigned to the account and is eligible to unbound.
- ensure!(!AccountRolesMapping::::contains_key(&account), Error::::HasRoleAssigned);
+ ensure!(Self::can_exit(account), Error::::HasRoleAssigned);
// Unbound funds.
let res = pallet_staking::Pallet::::unbond(origin, amount);
@@ -313,12 +340,9 @@ pub mod pallet {
#[pallet::weight({0})]
#[pallet::call_index(4)]
pub fn withdraw_unbonded(origin: OriginFor) -> DispatchResult {
- let stash_account = ensure_signed(origin.clone())?;
+ let account = ensure_signed(origin.clone())?;
// Ensure no role is assigned to the account and is eligible to withdraw.
- ensure!(
- !AccountRolesMapping::::contains_key(&stash_account),
- Error::::HasRoleAssigned
- );
+ ensure!(Self::can_exit(account), Error::::HasRoleAssigned);
// Withdraw unbound funds.
let res = pallet_staking::Pallet::::withdraw_unbonded(origin, 0);
diff --git a/pallets/roles/src/mock.rs b/pallets/roles/src/mock.rs
index ae4141095..8ac61a502 100644
--- a/pallets/roles/src/mock.rs
+++ b/pallets/roles/src/mock.rs
@@ -200,13 +200,9 @@ impl pallet_staking::Config for Runtime {
type WeightInfo = ();
}
-parameter_types! {
- pub const RolesPalletId: PalletId = PalletId(*b"py/roles");
-}
-
impl Config for Runtime {
type RuntimeEvent = RuntimeEvent;
- type PalletId = RolesPalletId;
+ type MaxRolesPerAccount = ConstU32<2>;
type WeightInfo = ();
}
diff --git a/pallets/roles/src/tests.rs b/pallets/roles/src/tests.rs
index f66576194..efdde5155 100644
--- a/pallets/roles/src/tests.rs
+++ b/pallets/roles/src/tests.rs
@@ -19,14 +19,14 @@ use frame_support::{assert_err, assert_ok};
use mock::*;
#[test]
-fn test_assign_role() {
+fn test_assign_roles() {
new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
// Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens
- assert_ok!(Roles::assign_role(
- RuntimeOrigin::signed(1),
- RoleType::Tss,
- ReStakingOption::Custom(5000)
- ));
+
+ // Roles user is interested in re-staking.
+ let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }];
+
+ assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records));
assert_events(vec![RuntimeEvent::Roles(crate::Event::RoleAssigned {
account: 1,
@@ -34,32 +34,70 @@ fn test_assign_role() {
})]);
// Lets verify role assigned to account.
- assert_eq!(Roles::account_role(1), Some(RoleType::Tss));
+ assert_eq!(Roles::has_role(1, RoleType::Tss), true);
// Verify ledger mapping
- assert_eq!(Roles::ledger(1), Some(RoleStakingLedger { stash: 1, total: 5000 }));
+ assert_eq!(
+ Roles::ledger(1),
+ Some(RoleStakingLedger {
+ stash: 1,
+ total: 5000,
+ roles: vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }]
+ })
+ );
});
}
-// Test that we can assign role with full staking option.
+// test assign multiple roles to an account.
#[test]
-fn test_assign_role_with_full_staking_option() {
+fn test_assign_multiple_roles() {
new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
// Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens
- assert_ok!(Roles::assign_role(
- RuntimeOrigin::signed(1),
- RoleType::Tss,
- ReStakingOption::Full
- ));
- assert_events(vec![RuntimeEvent::Roles(crate::Event::RoleAssigned {
- account: 1,
- role: RoleType::Tss,
- })]);
+ // Roles user is interested in re-staking.
+ let role_records = vec![
+ RoleStakingRecord { role: RoleType::Tss, re_staked: 2500 },
+ RoleStakingRecord { role: RoleType::ZkSaas, re_staked: 2500 },
+ ];
+
+ assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records));
// Lets verify role assigned to account.
- assert_eq!(Roles::account_role(1), Some(RoleType::Tss));
- // Verify ledger mapping
- assert_eq!(Roles::ledger(1), Some(RoleStakingLedger { stash: 1, total: 10000 }));
+ assert_eq!(Roles::has_role(1, RoleType::Tss), true);
+
+ // Lets verify role assigned to account.
+ assert_eq!(Roles::has_role(1, RoleType::ZkSaas), true);
+
+ assert_eq!(
+ Roles::ledger(1),
+ Some(RoleStakingLedger {
+ stash: 1,
+ total: 5000,
+ roles: vec![
+ RoleStakingRecord { role: RoleType::Tss, re_staked: 2500 },
+ RoleStakingRecord { role: RoleType::ZkSaas, re_staked: 2500 },
+ ]
+ })
+ );
+ });
+}
+
+// Test assign roles, should fail if total re-stake value exceeds max re-stake value.
+// Max re-stake value is 5000 (50% of total staked value).
+#[test]
+fn test_assign_roles_should_fail_if_total_re_stake_value_exceeds_max_re_stake_value() {
+ new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
+ // Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens
+
+ // Roles user is interested in re-staking.
+ let role_records = vec![
+ RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 },
+ RoleStakingRecord { role: RoleType::ZkSaas, re_staked: 5000 },
+ ];
+ // Since max re_stake limit is 5000 it should fail with `ExceedsMaxReStakeValue` error.
+ assert_err!(
+ Roles::assign_roles(RuntimeOrigin::signed(1), role_records),
+ Error::::ExceedsMaxReStakeValue
+ );
});
}
@@ -67,11 +105,11 @@ fn test_assign_role_with_full_staking_option() {
fn test_clear_role() {
new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
// Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens
- assert_ok!(Roles::assign_role(
- RuntimeOrigin::signed(1),
- RoleType::Tss,
- ReStakingOption::Custom(5000)
- ));
+
+ // Roles user is interested in re-staking.
+ let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }];
+
+ assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records));
// Now lets clear the role
assert_ok!(Roles::clear_role(RuntimeOrigin::signed(1), RoleType::Tss));
@@ -82,7 +120,7 @@ fn test_clear_role() {
})]);
// Role should be removed from account role mappings.
- assert_eq!(Roles::account_role(1), None);
+ assert_eq!(Roles::has_role(1, RoleType::Tss), false);
// Ledger should be removed from ledger mappings.
assert_eq!(Roles::ledger(1), None);
@@ -90,15 +128,15 @@ fn test_clear_role() {
}
#[test]
-fn test_assign_role_should_fail_if_not_validator() {
+fn test_assign_roles_should_fail_if_not_validator() {
new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
// we will use account 5 which is not a validator
+
+ // Roles user is interested in re-staking.
+ let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }];
+
assert_err!(
- Roles::assign_role(
- RuntimeOrigin::signed(5),
- RoleType::Tss,
- ReStakingOption::Custom(5000)
- ),
+ Roles::assign_roles(RuntimeOrigin::signed(5), role_records),
Error::::NotValidator
);
});
@@ -109,20 +147,20 @@ fn test_unbound_funds_should_work() {
new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
// Initially validator account has staked 10_000 tokens and wants to re-stake 5000 tokens
// for providing TSS services.
- assert_ok!(Roles::assign_role(
- RuntimeOrigin::signed(1),
- RoleType::Tss,
- ReStakingOption::Custom(5000)
- ));
+
+ // Roles user is interested in re-staking.
+ let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }];
+
+ assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records));
// Lets verify role is assigned to account.
- assert_eq!(Roles::account_role(1), Some(RoleType::Tss));
+ assert_eq!(Roles::has_role(1, RoleType::Tss), true);
// Lets clear the role.
assert_ok!(Roles::clear_role(RuntimeOrigin::signed(1), RoleType::Tss));
// Role should be removed from account role mappings.
- assert_eq!(Roles::account_role(1), None);
+ assert_eq!(Roles::has_role(1, RoleType::Tss), false);
// unbound funds.
assert_ok!(Roles::unbound_funds(RuntimeOrigin::signed(1), 5000));
@@ -145,14 +183,14 @@ fn test_unbound_funds_should_fail_if_role_assigned() {
new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| {
// Initially validator account has staked 10_000 tokens and wants to re-stake 5000 tokens
// for providing TSS services.
- assert_ok!(Roles::assign_role(
- RuntimeOrigin::signed(1),
- RoleType::Tss,
- ReStakingOption::Custom(5000)
- ));
+
+ // Roles user is interested in re-staking.
+ let role_records = vec![RoleStakingRecord { role: RoleType::Tss, re_staked: 5000 }];
+
+ assert_ok!(Roles::assign_roles(RuntimeOrigin::signed(1), role_records));
// Lets verify role is assigned to account.
- assert_eq!(Roles::account_role(1), Some(RoleType::Tss));
+ assert_eq!(Roles::has_role(1, RoleType::Tss), true);
// Lets try to unbound funds.
assert_err!(