From 1ab7dad33bfaedece997abf371464f6371b5df1f Mon Sep 17 00:00:00 2001 From: Salman Pathan Date: Wed, 15 Nov 2023 20:56:35 +0530 Subject: [PATCH] Validator re-staking (#297) * validator checks before assigning role * block unbond call from pallet-staking and allow unbonding through roles-pallet * update errors * update docs * add new tests * remove slashing test * Option to choose re-stake value (Full or custom) * handle unwrap * cargo fmt * block chill and withdraw-unbond calls * update events * cargo fmt --all * convert DispatchErrorWithPostInfo to DispatchError --- Cargo.lock | 6 + pallets/roles/Cargo.toml | 18 ++- pallets/roles/src/impls.rs | 34 +---- pallets/roles/src/lib.rs | 225 ++++++++++++++++++++++------------ pallets/roles/src/mock.rs | 193 +++++++++++++++++++++++++++-- pallets/roles/src/tests.rs | 173 +++++++++++++++++++------- primitives/src/types/roles.rs | 9 ++ standalone/runtime/src/lib.rs | 22 ++++ 8 files changed, 518 insertions(+), 162 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7e0ec86ef..706b22215 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9059,10 +9059,14 @@ name = "pallet-roles" version = "0.5.0" dependencies = [ "frame-benchmarking", + "frame-election-provider-support", "frame-support", "frame-system", "hex-literal 0.3.4", "pallet-balances", + "pallet-session", + "pallet-staking", + "pallet-timestamp", "parity-scale-codec", "scale-info", "serde", @@ -9070,6 +9074,7 @@ dependencies = [ "sp-core 21.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "sp-io 23.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "sp-runtime 24.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", + "sp-staking", "sp-std 8.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", "tangle-primitives", ] @@ -9125,6 +9130,7 @@ dependencies = [ "pallet-authorship", "pallet-session", "parity-scale-codec", + "rand_chacha 0.2.2", "scale-info", "serde", "sp-application-crypto 23.0.0 (git+https://github.com/paritytech/substrate?branch=polkadot-v1.0.0)", diff --git a/pallets/roles/Cargo.toml b/pallets/roles/Cargo.toml index a7741be0c..5c41cc474 100644 --- a/pallets/roles/Cargo.toml +++ b/pallets/roles/Cargo.toml @@ -17,11 +17,16 @@ serde = { workspace = true } frame-support = { workspace = true } frame-system = { workspace = true } pallet-balances = { workspace = true } +pallet-staking = { workspace = true } +pallet-session = { workspace = true } +pallet-timestamp = { workspace = true } sp-io = { workspace = true } sp-runtime = { workspace = true } sp-std = { workspace = true } +sp-staking = { workspace = true } tangle-primitives = {workspace = true, default-features = false } frame-benchmarking = { workspace = true, optional = true } +frame-election-provider-support = { workspace = true } [dev-dependencies] hex-literal = { workspace = true } @@ -40,8 +45,13 @@ std = [ "sp-runtime/std", "sp-std/std", "sp-io/std", + "sp-staking/std", "pallet-balances/std", - "tangle-primitives/std" + "pallet-staking/std", + "pallet-session/std", + "pallet-timestamp/std", + "tangle-primitives/std", + "frame-election-provider-support/std", ] try-runtime = ["frame-support/try-runtime"] @@ -50,5 +60,9 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "sp-runtime/runtime-benchmarks", - "pallet-balances/runtime-benchmarks" + "sp-staking/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "pallet-staking/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "frame-election-provider-support/runtime-benchmarks", ] diff --git a/pallets/roles/src/impls.rs b/pallets/roles/src/impls.rs index 28554650d..e465d58fb 100644 --- a/pallets/roles/src/impls.rs +++ b/pallets/roles/src/impls.rs @@ -15,7 +15,6 @@ // along with Tangle. If not, see . use super::*; -use frame_support::{pallet_prelude::DispatchResult, traits::WithdrawReasons}; use sp_runtime::Saturating; use tangle_primitives::{roles::RoleType, traits::roles::RolesHandler}; @@ -73,7 +72,7 @@ impl Pallet { /// The total amount of the balance that can be slashed. pub fn slashable_balance_of(stash: &T::AccountId) -> BalanceOf { // Weight note: consider making the stake accessible through stash. - Self::ledger(&stash).map(|l| l.total_locked).unwrap_or_default() + Self::ledger(&stash).map(|l| l.total).unwrap_or_default() } /// Slash the given amount from the stash account. @@ -85,9 +84,9 @@ impl Pallet { address: T::AccountId, slash_amount: T::CurrencyBalance, ) -> sp_runtime::DispatchResult { - let mut ledger = Self::ledger(&address).ok_or(Error::::InvalidStashController)?; + let mut ledger = Self::ledger(&address).ok_or(Error::::AccountNotPaired)?; let (_imbalance, _missing) = T::Currency::slash(&address, slash_amount.into()); - ledger.total_locked = ledger.total_locked.saturating_sub(slash_amount.into()); + ledger.total = ledger.total.saturating_sub(slash_amount.into()); Self::update_ledger(&address, &ledger); Self::deposit_event(Event::Slashed { account: address, amount: slash_amount }); Ok(()) @@ -102,36 +101,11 @@ impl Pallet { /// # Note /// This function will set a lock on the stash account. pub(crate) fn update_ledger(staker: &T::AccountId, ledger: &RoleStakingLedger) { - T::Currency::set_lock( - ROLES_STAKING_ID, - &ledger.stash, - ledger.total_locked, - WithdrawReasons::all(), - ); >::insert(staker, ledger); } /// Kill the stash account and remove all related information. - pub(crate) fn kill_stash(stash: &T::AccountId) -> DispatchResult { + pub(crate) fn kill_stash(stash: &T::AccountId) { >::remove(&stash); - Ok(()) - } - - /// Unbond the stash account. - /// - /// # Parameters - /// - `ledger`: The ledger of the stash account. - /// - /// # Note - /// This function will remove the lock on the stash account. - pub(super) fn unbond(ledger: &RoleStakingLedger) -> DispatchResult { - let stash = ledger.stash.clone(); - if ledger.total_locked > T::Currency::minimum_balance() { - // Remove the lock. - T::Currency::remove_lock(ROLES_STAKING_ID, &stash); - // Kill the stash and related information. - Self::kill_stash(&stash)?; - } - Ok(()) } } diff --git a/pallets/roles/src/lib.rs b/pallets/roles/src/lib.rs index dc90a9c07..ad643a145 100644 --- a/pallets/roles/src/lib.rs +++ b/pallets/roles/src/lib.rs @@ -21,15 +21,19 @@ use codec::MaxEncodedLen; use frame_support::{ ensure, - traits::{Currency, Get, LockIdentifier, LockableCurrency, OnUnbalanced}, + traits::{Currency, Get}, CloneNoBound, EqNoBound, PalletId, PartialEqNoBound, RuntimeDebugNoBound, }; + pub use pallet::*; use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use sp_runtime::{codec, traits::Zero}; use sp_std::{convert::TryInto, prelude::*, vec}; -use tangle_primitives::{roles::RoleType, traits::roles::RolesHandler}; +use tangle_primitives::{ + roles::{ReStakingOption, RoleType}, + traits::roles::RolesHandler, +}; mod impls; #[cfg(test)] pub(crate) mod mock; @@ -53,32 +57,27 @@ 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 we are currently accounting for. - /// It's just `active` plus all the `unlocking` balances. + /// The total amount of the stash's balance that is re-staked for selected services + /// This re-staked balance we are currently accounting for new slashing conditions. #[codec(compact)] - pub total_locked: BalanceOf, + pub total: BalanceOf, } impl RoleStakingLedger { /// Initializes the default object using the given `validator`. pub fn default_from(stash: T::AccountId) -> Self { - Self { stash, total_locked: Zero::zero() } + Self { stash, total: Zero::zero() } } /// Returns `true` if the stash account has no funds at all. pub fn is_empty(&self) -> bool { - self.total_locked.is_zero() + self.total.is_zero() } } -pub type CurrencyOf = ::Currency; +pub type CurrencyOf = ::Currency; pub type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; -type NegativeImbalanceOf = <::Currency as Currency< - ::AccountId, ->>::NegativeImbalance; - -const ROLES_STAKING_ID: LockIdentifier = *b"rstaking"; + as Currency<::AccountId>>::Balance; #[frame_support::pallet] pub mod pallet { @@ -92,30 +91,10 @@ pub mod pallet { /// Configuration trait. #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config + pallet_staking::Config { /// The overarching event type. type RuntimeEvent: From> + IsType<::RuntimeEvent>; - type Currency: LockableCurrency< - Self::AccountId, - Moment = BlockNumberFor, - Balance = Self::CurrencyBalance, - >; - /// Just the `Currency::Balance` type; we have this item to allow us to constrain it to - /// `From`. - type CurrencyBalance: sp_runtime::traits::AtLeast32BitUnsigned - + codec::FullCodec - + Copy - + MaybeSerializeDeserialize - + sp_std::fmt::Debug - + Default - + From - + TypeInfo - + MaxEncodedLen; - - /// Handler for the unbalanced reduction when slashing a staker. - type Slash: OnUnbalanced>; - type WeightInfo: WeightInfo; #[pallet::constant] @@ -129,26 +108,26 @@ pub mod pallet { RoleAssigned { account: T::AccountId, role: RoleType }, /// Removed validator from role. RoleRemoved { account: T::AccountId, role: RoleType }, - /// Funds bonded to become a validator. - Bonded { account: T::AccountId, amount: BalanceOf }, - /// Funds unbonded to stop being a validator. - Unbonded { account: T::AccountId, amount: BalanceOf }, /// Slashed validator. Slashed { account: T::AccountId, amount: BalanceOf }, } #[pallet::error] pub enum Error { - /// Role has already been assigned to provided validator. - RoleAlreadyAssigned, + /// Not a validator. + NotValidator, + /// Validator has active role assigned + HasRoleAssigned, /// No role assigned to provided validator. - RoleNotAssigned, - /// Insufficient bond to become a validator. - InsufficientBond, - /// Stash controller account already added to Ledger - AlreadyPaired, - /// Stash controller account not found in ledger - InvalidStashController, + NoRoleAssigned, + /// Invalid Re-staking amount, should not exceed total staked amount. + InvalidReStakingBond, + /// Re staking amount should be greater than minimum re-staking bond requirement. + InsufficientReStakingBond, + /// Stash controller account already added to Roles Ledger + AccountAlreadyPaired, + /// Stash controller account not found in Roles Ledger. + AccountNotPaired, } /// Map from all "controller" accounts to the info regarding the staking. @@ -162,58 +141,66 @@ pub mod pallet { /// Mapping of resource to bridge index pub type AccountRolesMapping = StorageMap<_, Blake2_256, T::AccountId, RoleType>; - /// The minimum active bond to become and maintain the role. + /// The minimum re staking bond to become and maintain the role. #[pallet::storage] #[pallet::getter(fn min_active_bond)] - pub(super) type MinActiveBond = StorageValue<_, BalanceOf, ValueQuery>; + pub(super) type MinReStakingBond = StorageValue<_, BalanceOf, ValueQuery>; /// Assigns a role to the validator. /// /// # Parameters /// /// - `origin`: Origin of the transaction. - /// - `bond_value`: Amount of funds to bond. /// - `role`: Role to assign to the validator. + /// - `re_stake`: Amount of funds you want to re-stake. /// /// This function will return error if + /// - Account is not a validator account. /// - Role is already assigned to the validator. - /// - Min active bond is not met. + /// - Min re-staking bond is not met. #[pallet::call] impl Pallet { #[pallet::weight({0})] #[pallet::call_index(0)] pub fn assign_role( origin: OriginFor, - #[pallet::compact] bond_value: BalanceOf, role: RoleType, + re_stake: ReStakingOption, ) -> DispatchResult { let stash_account = ensure_signed(origin)?; + // Ensure stash account is a validator. + ensure!( + pallet_staking::Validators::::contains_key(&stash_account), + Error::::NotValidator + ); + // Check if role is already assigned. ensure!( !AccountRolesMapping::::contains_key(&stash_account), - Error::::RoleAlreadyAssigned + Error::::HasRoleAssigned ); - // Check if stash account is already paired. - ensure!(!>::contains_key(&stash_account), Error::::AlreadyPaired); + // Check if stash account is already paired/ re-staked. + ensure!(!>::contains_key(&stash_account), Error::::AccountAlreadyPaired); - // Check if min active bond is met. - let min_active_bond = MinActiveBond::::get(); - ensure!(bond_value > min_active_bond.into(), Error::::InsufficientBond); + 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(), + }; - // Bond with stash account. - let stash_balance = T::Currency::free_balance(&stash_account); - let value = bond_value.min(stash_balance); + // 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); // Update ledger. - let item = RoleStakingLedger { stash: stash_account.clone(), total_locked: value }; + let item = RoleStakingLedger { stash: stash_account.clone(), total: re_stake_amount }; Self::update_ledger(&stash_account, &item); - Self::deposit_event(Event::::Bonded { - account: stash_account.clone(), - amount: value, - }); - // Add role mapping for the stash account. AccountRolesMapping::::insert(&stash_account, role); Self::deposit_event(Event::::RoleAssigned { account: stash_account.clone(), role }); @@ -228,33 +215,117 @@ pub mod pallet { /// - `role`: Role to remove from the validator. /// /// This function will return error if + /// - Account is not a validator account. /// - Role is not assigned to the validator. + /// - All the jobs are not completed. #[pallet::weight({0})] #[pallet::call_index(1)] pub fn clear_role(origin: OriginFor, role: RoleType) -> DispatchResult { let stash_account = ensure_signed(origin)?; + // Ensure stash account is a validator. + ensure!( + pallet_staking::Validators::::contains_key(&stash_account), + Error::::NotValidator + ); + // check if role is assigned. ensure!( Self::is_validator(stash_account.clone(), role.clone()), - Error::::RoleNotAssigned + 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 - // Unbound locked funds. - let ledger = Self::ledger(&stash_account).ok_or(Error::::InvalidStashController)?; - Self::unbond(&ledger)?; - Self::deposit_event(Event::::Unbonded { - account: ledger.stash, - amount: ledger.total_locked, - }); - // Remove role from the mapping. AccountRolesMapping::::remove(&stash_account); + // Remove stash account related info. + Self::kill_stash(&stash_account); + Self::deposit_event(Event::::RoleRemoved { account: stash_account, role }); Ok(()) } + + /// Declare no desire to either validate or nominate. + /// + /// If you have opted for any of the roles, please submit `clear_role` extrinsic to opt out + /// of all the services. Once your role is cleared, your request will be processed. + /// + /// # Parameters + /// + /// - `origin`: Origin of the transaction. + /// + /// This function will return error if + /// - Account is not a validator account. + /// - Role is assigned to the validator. + #[pallet::weight({0})] + #[pallet::call_index(2)] + 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); + + // chill + pallet_staking::Pallet::::chill(origin) + } + + /// Unbound funds from the stash account. + /// This will allow user to unbound and later withdraw funds. + /// If you have opted for any of the roles, please submit `clear_role` extrinsic to opt out + /// of all the services. Once your role is cleared, you can unbound + /// and withdraw funds. + /// + /// # Parameters + /// + /// - `origin`: Origin of the transaction. + /// - `amount`: Amount of funds to unbound. + /// + /// This function will return error if + /// - If there is any active role assigned to the user. + /// + #[pallet::weight({0})] + #[pallet::call_index(3)] + pub fn unbound_funds( + origin: OriginFor, + #[pallet::compact] amount: BalanceOf, + ) -> 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); + + // Unbound funds. + let res = pallet_staking::Pallet::::unbond(origin, amount); + match res { + Ok(_) => Ok(()), + Err(dispatch_post_info) => Err(dispatch_post_info.error), + } + } + + /// Withdraw unbound funds after un-bonding period has passed. + /// + /// # Parameters + /// + /// - `origin`: Origin of the transaction. + /// + /// This function will return error if + /// - If there is any active role assigned to the user. + #[pallet::weight({0})] + #[pallet::call_index(4)] + pub fn withdraw_unbonded(origin: OriginFor) -> DispatchResult { + let stash_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 + ); + + // Withdraw unbound funds. + let res = pallet_staking::Pallet::::withdraw_unbonded(origin, 0); + match res { + Ok(_) => Ok(()), + Err(dispatch_post_info) => Err(dispatch_post_info.error), + } + } } } diff --git a/pallets/roles/src/mock.rs b/pallets/roles/src/mock.rs index f4cc2c38c..ae4141095 100644 --- a/pallets/roles/src/mock.rs +++ b/pallets/roles/src/mock.rs @@ -17,14 +17,19 @@ use super::*; use crate as pallet_roles; +use frame_election_provider_support::{onchain, SequentialPhragmen}; use frame_support::{ construct_runtime, parameter_types, - traits::{ConstU128, ConstU32, ConstU64, Everything}, + traits::{ConstU128, ConstU32, ConstU64, Contains, Hooks}, }; - +use pallet_session::TestSessionHandler; use sp_core::H256; -use sp_runtime::{traits::IdentityLookup, BuildStorage}; -pub type AccountId = u128; +use sp_runtime::{ + testing::{Header, UintAuthorityId}, + traits::IdentityLookup, + BuildStorage, Perbill, +}; +pub type AccountId = u64; pub type Balance = u128; impl frame_system::Config for Runtime { @@ -46,7 +51,7 @@ impl frame_system::Config for Runtime { type OnNewAccount = (); type OnKilledAccount = (); type DbWeight = (); - type BaseCallFilter = Everything; + type BaseCallFilter = BaseFilter; type SystemWeightInfo = (); type SS58Prefix = (); type OnSetCode = (); @@ -69,20 +74,144 @@ impl pallet_balances::Config for Runtime { type MaxFreezes = (); } +impl pallet_timestamp::Config for Runtime { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = ConstU64<5>; + type WeightInfo = (); +} + +impl pallet_session::historical::Config for Runtime { + type FullIdentification = pallet_staking::Exposure; + type FullIdentificationOf = pallet_staking::ExposureOf; +} + +pub struct BaseFilter; +impl Contains for BaseFilter { + fn contains(call: &RuntimeCall) -> bool { + let is_stake_unbound_call = + matches!(call, RuntimeCall::Staking(pallet_staking::Call::unbond { .. })); + + if is_stake_unbound_call { + // no unbond call + return false + } + + // no chill call + if matches!(call, RuntimeCall::Staking(pallet_staking::Call::chill { .. })) { + return false + } + + // no withdraw_unbonded call + let is_stake_withdraw_call = + matches!(call, RuntimeCall::Staking(pallet_staking::Call::withdraw_unbonded { .. })); + + if is_stake_withdraw_call { + return false + } + + true + } +} + +sp_runtime::impl_opaque_keys! { + pub struct MockSessionKeys { + pub dummy: UintAuthorityId, + } +} + +impl From for MockSessionKeys { + fn from(dummy: UintAuthorityId) -> Self { + Self { dummy } + } +} + +pub struct MockSessionManager; + +impl pallet_session::SessionManager for MockSessionManager { + fn end_session(_: sp_staking::SessionIndex) {} + fn start_session(_: sp_staking::SessionIndex) {} + fn new_session(idx: sp_staking::SessionIndex) -> Option> { + if idx == 0 || idx == 1 { + Some(vec![1, 2, 3, 4]) + } else if idx == 2 { + Some(vec![1, 2, 3, 4]) + } else { + None + } + } +} + parameter_types! { - pub const RolesPalletId: PalletId = PalletId(*b"py/roles"); + pub const Period: u64 = 1; + pub const Offset: u64 = 0; } -impl Config for Runtime { +impl pallet_session::Config for Runtime { + type SessionManager = MockSessionManager; + type Keys = MockSessionKeys; + type ShouldEndSession = pallet_session::PeriodicSessions; + type NextSessionRotation = pallet_session::PeriodicSessions; + type SessionHandler = TestSessionHandler; type RuntimeEvent = RuntimeEvent; + type ValidatorId = AccountId; + type ValidatorIdOf = pallet_staking::StashOf; + type WeightInfo = (); +} + +pub struct OnChainSeqPhragmen; +impl onchain::Config for OnChainSeqPhragmen { + type System = Runtime; + type Solver = SequentialPhragmen; + type DataProvider = Staking; + type WeightInfo = (); + type MaxWinners = ConstU32<100>; + type VotersBound = ConstU32<{ u32::MAX }>; + type TargetsBound = ConstU32<{ u32::MAX }>; +} + +impl pallet_staking::Config for Runtime { + type MaxNominations = ConstU32<16>; type Currency = Balances; type CurrencyBalance = ::Balance; + type UnixTime = pallet_timestamp::Pallet; + type CurrencyToVote = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; type Slash = (); + type Reward = (); + type SessionsPerEra = (); + type SlashDeferDuration = (); + type AdminOrigin = frame_system::EnsureRoot; + type BondingDuration = (); + type SessionInterface = Self; + type EraPayout = (); + type NextNewSession = Session; + type MaxNominatorRewardedPerValidator = ConstU32<64>; + type OffendingValidatorsThreshold = (); + type ElectionProvider = onchain::OnChainExecution; + type GenesisElectionProvider = Self::ElectionProvider; + type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; + type TargetList = pallet_staking::UseValidatorsMap; + type MaxUnlockingChunks = ConstU32<32>; + type HistoryDepth = ConstU32<84>; + type EventListeners = (); + type BenchmarkingConfig = pallet_staking::TestBenchmarkingConfig; + type WeightInfo = (); +} + +parameter_types! { + pub const RolesPalletId: PalletId = PalletId(*b"py/roles"); +} + +impl Config for Runtime { + type RuntimeEvent = RuntimeEvent; type PalletId = RolesPalletId; type WeightInfo = (); } -type Block = frame_system::mocking::MockBlock; +pub type Block = sp_runtime::generic::Block; +pub type UncheckedExtrinsic = sp_runtime::generic::UncheckedExtrinsic; construct_runtime!( pub enum Runtime @@ -90,6 +219,8 @@ construct_runtime!( System: frame_system, Balances: pallet_balances, Roles: pallet_roles, + Session: pallet_session, + Staking: pallet_staking, } ); @@ -116,14 +247,56 @@ pub fn assert_events(mut expected: Vec) { // This function basically just builds a genesis storage key/value store according to // our desired mockup. -pub fn new_test_ext() -> sp_io::TestExternalities { +pub fn new_test_ext_raw_authorities(authorities: Vec) -> sp_io::TestExternalities { let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); // We use default for brevity, but you can configure as desired if needed. - pallet_balances::GenesisConfig:: { balances: vec![(10, 10000), (20, 10000)] } + let balances: Vec<_> = authorities.iter().map(|i| (*i, 20_000_u128)).collect(); + + pallet_balances::GenesisConfig:: { balances } .assimilate_storage(&mut t) .unwrap(); + let session_keys: Vec<_> = authorities + .iter() + .map(|id| (*id, *id, MockSessionKeys { dummy: UintAuthorityId(*id) })) + .collect(); + + pallet_session::GenesisConfig:: { keys: session_keys } + .assimilate_storage(&mut t) + .unwrap(); + + let stakers: Vec<_> = authorities + .iter() + .map(|authority| { + ( + *authority, + *authority, + 10_000_u128, + pallet_staking::StakerStatus::::Validator, + ) + }) + .collect(); + + let staking_config = pallet_staking::GenesisConfig:: { + stakers, + validator_count: 4, + force_era: pallet_staking::Forcing::ForceNew, + minimum_validator_count: 0, + max_validator_count: Some(5), + max_nominator_count: Some(5), + invulnerables: vec![], + ..Default::default() + }; + + staking_config.assimilate_storage(&mut t).unwrap(); + let mut ext = sp_io::TestExternalities::new(t); ext.execute_with(|| System::set_block_number(1)); + ext.execute_with(|| { + System::set_block_number(1); + Session::on_initialize(1); + >::on_initialize(1); + }); + ext } diff --git a/pallets/roles/src/tests.rs b/pallets/roles/src/tests.rs index 38abccadd..f66576194 100644 --- a/pallets/roles/src/tests.rs +++ b/pallets/roles/src/tests.rs @@ -15,78 +15,165 @@ // along with Tangle. If not, see . #![cfg(test)] use super::*; -use frame_support::assert_ok; +use frame_support::{assert_err, assert_ok}; use mock::*; -use tangle_primitives::jobs::ValidatorOffence; #[test] fn test_assign_role() { - new_test_ext().execute_with(|| { + 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(10), 5000, RoleType::Tss)); + assert_ok!(Roles::assign_role( + RuntimeOrigin::signed(1), + RoleType::Tss, + ReStakingOption::Custom(5000) + )); - assert_events(vec![ - RuntimeEvent::Roles(crate::Event::Bonded { account: 10, amount: 5000 }), - RuntimeEvent::Roles(crate::Event::RoleAssigned { account: 10, role: RoleType::Tss }), - ]); + assert_events(vec![RuntimeEvent::Roles(crate::Event::RoleAssigned { + account: 1, + role: RoleType::Tss, + })]); // Lets verify role assigned to account. - assert_eq!(Roles::account_role(10), Some(RoleType::Tss)); + assert_eq!(Roles::account_role(1), Some(RoleType::Tss)); // Verify ledger mapping - assert_eq!(Roles::ledger(10), Some(RoleStakingLedger { stash: 10, total_locked: 5000 })); - // Verify total usable balance of the account. Since we have bonded 5000 tokens, we should - // have 5000 tokens usable. - assert_eq!(Balances::usable_balance(10), 5000); + assert_eq!(Roles::ledger(1), Some(RoleStakingLedger { stash: 1, total: 5000 })); + }); +} + +// Test that we can assign role with full staking option. +#[test] +fn test_assign_role_with_full_staking_option() { + 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, + })]); + + // 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 })); }); } #[test] fn test_clear_role() { - new_test_ext().execute_with(|| { + 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(10), 5000, RoleType::Tss)); - // Verify total usable balance of the account. Since we have bonded 5000 tokens, we should - // have 5000 tokens usable. - assert_eq!(Balances::usable_balance(10), 5000); + assert_ok!(Roles::assign_role( + RuntimeOrigin::signed(1), + RoleType::Tss, + ReStakingOption::Custom(5000) + )); // Now lets clear the role - assert_ok!(Roles::clear_role(RuntimeOrigin::signed(10), RoleType::Tss)); + assert_ok!(Roles::clear_role(RuntimeOrigin::signed(1), RoleType::Tss)); - assert_events(vec![ - RuntimeEvent::Roles(crate::Event::Unbonded { account: 10, amount: 5000 }), - RuntimeEvent::Roles(crate::Event::RoleRemoved { account: 10, role: RoleType::Tss }), - ]); + assert_events(vec![RuntimeEvent::Roles(crate::Event::RoleRemoved { + account: 1, + role: RoleType::Tss, + })]); // Role should be removed from account role mappings. - assert_eq!(Roles::account_role(10), None); + assert_eq!(Roles::account_role(1), None); // Ledger should be removed from ledger mappings. - assert_eq!(Roles::ledger(10), None); + assert_eq!(Roles::ledger(1), None); + }); +} - // Verify total usable balance of the account. Since we have cleared the role, we should - // have 10000 tokens usable. - assert_eq!(Balances::usable_balance(10), 10000); +#[test] +fn test_assign_role_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 + assert_err!( + Roles::assign_role( + RuntimeOrigin::signed(5), + RoleType::Tss, + ReStakingOption::Custom(5000) + ), + Error::::NotValidator + ); }); } -// test slashing #[test] -fn test_slash_validator() { - new_test_ext().execute_with(|| { - // Initially account if funded with 10000 tokens and we are trying to bond 5000 tokens - assert_ok!(Roles::assign_role(RuntimeOrigin::signed(10), 5000, RoleType::Tss)); - // Verify total usable balance of the account. Since we have bonded 5000 tokens, we should - // have 5000 tokens usable. - assert_eq!(Balances::usable_balance(10), 5000); +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) + )); + + // Lets verify role is assigned to account. + assert_eq!(Roles::account_role(1), Some(RoleType::Tss)); + + // 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); + + // unbound funds. + assert_ok!(Roles::unbound_funds(RuntimeOrigin::signed(1), 5000)); + + assert_events(vec![RuntimeEvent::Staking(pallet_staking::Event::Unbonded { + stash: 1, + amount: 5000, + })]); + + // Get pallet staking ledger mapping. + let staking_ledger = pallet_staking::Ledger::::get(1).unwrap(); + // Since we we have unbounded 5000 tokens, we should have 5000 tokens in staking ledger. + assert_eq!(staking_ledger.active, 5000); + }); +} + +// Test unbound should fail if role is assigned to account. +#[test] +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) + )); + + // Lets verify role is assigned to account. + assert_eq!(Roles::account_role(1), Some(RoleType::Tss)); + + // Lets try to unbound funds. + assert_err!( + Roles::unbound_funds(RuntimeOrigin::signed(1), 5000), + Error::::HasRoleAssigned + ); + }); +} + +// Test unbound should work if no role assigned to account. +#[test] +fn test_unbound_funds_should_work_if_no_role_assigned() { + new_test_ext_raw_authorities(vec![1, 2, 3, 4]).execute_with(|| { + // Initially validator account has staked 10_000 tokens. - // Now lets slash the account for being Inactive. - assert_ok!(Roles::slash_validator(10, ValidatorOffence::Inactivity)); + // Since validator has not opted for any roles, he should be able to unbound his funds. + assert_ok!(Roles::unbound_funds(RuntimeOrigin::signed(1), 5000)); - assert_events(vec![RuntimeEvent::Roles(crate::Event::Slashed { - account: 10, - amount: 1000, + assert_events(vec![RuntimeEvent::Staking(pallet_staking::Event::Unbonded { + stash: 1, + amount: 5000, })]); - // should be updated in ledger - assert_eq!(Roles::ledger(10), Some(RoleStakingLedger { stash: 10, total_locked: 4000 })); }); } diff --git a/primitives/src/types/roles.rs b/primitives/src/types/roles.rs index 3d4c9842a..e69212299 100644 --- a/primitives/src/types/roles.rs +++ b/primitives/src/types/roles.rs @@ -46,3 +46,12 @@ impl From for RoleType { } } } + +/// Role type to be used in the system. +#[derive(Encode, Decode, Clone, Copy, Debug, PartialEq, Eq, TypeInfo)] +pub enum ReStakingOption { + // Re-stake all the staked funds for selected role. + Full, + // Re-stake only the given amount of funds for selected role. + Custom(u64), +} diff --git a/standalone/runtime/src/lib.rs b/standalone/runtime/src/lib.rs index 85911cbce..887db12f4 100644 --- a/standalone/runtime/src/lib.rs +++ b/standalone/runtime/src/lib.rs @@ -1097,6 +1097,28 @@ impl Contains for BaseFilter { // no paused call return false } + // Following staking pallet calls will be blocked and will be allowed to execute + // through role pallet. + let is_stake_unbound_call = + matches!(call, RuntimeCall::Staking(pallet_staking::Call::unbond { .. })); + + if is_stake_unbound_call { + // no unbond call + return false + } + + // no chill call + if matches!(call, RuntimeCall::Staking(pallet_staking::Call::chill { .. })) { + return false + } + + // no withdraw_unbonded call + let is_stake_withdraw_call = + matches!(call, RuntimeCall::Staking(pallet_staking::Call::withdraw_unbonded { .. })); + + if is_stake_withdraw_call { + return false + } let democracy_related = matches!( call,