From 967bbae910be5b5753adf015a8dc6a41ea4af427 Mon Sep 17 00:00:00 2001 From: 1xstj <106580853+1xstj@users.noreply.github.com> Date: Fri, 22 Nov 2024 05:36:02 +0000 Subject: [PATCH] feat: Improve traits for slashing (#829) * add new function to balances precompile * fix consesus data provider * delegator stake on blueprint * clean slate * update flow * fix: fix delegation error * tests passing * cleanup merge * test passing * cleanup * clippy fix * setup trait * impl trait * tests passing * cleanup clippy * setup recipient * cleanup clippy --- .../src/functions/delegate.rs | 69 ++++++++++ .../src/functions/operator.rs | 43 ++++++ pallets/multi-asset-delegation/src/lib.rs | 17 ++- pallets/multi-asset-delegation/src/mock.rs | 2 + .../src/tests/operator.rs | 125 ++++++++++++++++++ pallets/multi-asset-delegation/src/traits.rs | 6 + .../src/types/delegator.rs | 3 +- pallets/services/src/mock.rs | 7 + .../multi-asset-delegation/src/mock.rs | 2 + precompiles/services/src/mock.rs | 7 + .../src/traits/multi_asset_delegation.rs | 6 + runtime/mainnet/src/lib.rs | 1 + runtime/testnet/src/lib.rs | 1 + 13 files changed, 286 insertions(+), 3 deletions(-) diff --git a/pallets/multi-asset-delegation/src/functions/delegate.rs b/pallets/multi-asset-delegation/src/functions/delegate.rs index 3cfe8a17..501a70aa 100644 --- a/pallets/multi-asset-delegation/src/functions/delegate.rs +++ b/pallets/multi-asset-delegation/src/functions/delegate.rs @@ -15,9 +15,14 @@ // along with Tangle. If not, see . use super::*; use crate::{types::*, Pallet}; +use frame_support::traits::fungibles::Mutate; +use frame_support::traits::tokens::Preservation; use frame_support::{ensure, pallet_prelude::DispatchResult, traits::Get}; use sp_runtime::traits::{CheckedSub, Zero}; +use sp_runtime::DispatchError; +use sp_runtime::Percent; use sp_std::vec::Vec; +use tangle_primitives::BlueprintId; impl Pallet { /// Processes the delegation of an amount of an asset to an operator. @@ -338,4 +343,68 @@ impl Pallet { Ok(()) }) } + + /// Slashes a delegator's stake. + /// + /// # Arguments + /// + /// * `delegator` - The account ID of the delegator. + /// * `operator` - The account ID of the operator. + /// * `blueprint_id` - The ID of the blueprint. + /// * `percentage` - The percentage of the stake to slash. + /// + /// # Errors + /// + /// Returns an error if the delegator is not found, or if the delegation is not active. + pub fn slash_delegator( + delegator: &T::AccountId, + operator: &T::AccountId, + blueprint_id: BlueprintId, + percentage: Percent, + ) -> Result<(), DispatchError> { + Delegators::::try_mutate(delegator, |maybe_metadata| { + let metadata = maybe_metadata.as_mut().ok_or(Error::::NotDelegator)?; + + let delegation = metadata + .delegations + .iter_mut() + .find(|d| &d.operator == operator) + .ok_or(Error::::NoActiveDelegation)?; + + // Check delegation type and blueprint_id + match &delegation.blueprint_selection { + DelegatorBlueprintSelection::Fixed(blueprints) => { + // For fixed delegation, ensure the blueprint_id is in the list + ensure!(blueprints.contains(&blueprint_id), Error::::BlueprintNotSelected); + }, + DelegatorBlueprintSelection::All => { + // For "All" type, no need to check blueprint_id + }, + } + + // Calculate and apply slash + let slash_amount = percentage.mul_floor(delegation.amount); + delegation.amount = delegation + .amount + .checked_sub(&slash_amount) + .ok_or(Error::::InsufficientStakeRemaining)?; + + // Transfer slashed amount to the treasury + let _ = T::Fungibles::transfer( + delegation.asset_id, + &Self::pallet_account(), + &T::SlashedAmountRecipient::get(), + slash_amount, + Preservation::Expendable, + ); + + // emit event + Self::deposit_event(Event::DelegatorSlashed { + who: delegator.clone(), + amount: slash_amount, + }); + + Ok(()) + }) + } } diff --git a/pallets/multi-asset-delegation/src/functions/operator.rs b/pallets/multi-asset-delegation/src/functions/operator.rs index 2567e2bf..1338b083 100644 --- a/pallets/multi-asset-delegation/src/functions/operator.rs +++ b/pallets/multi-asset-delegation/src/functions/operator.rs @@ -17,6 +17,8 @@ /// Functions for the pallet. use super::*; use crate::{types::*, Pallet}; +use frame_support::traits::Currency; +use frame_support::traits::ExistenceRequirement; use frame_support::BoundedVec; use frame_support::{ ensure, @@ -25,6 +27,8 @@ use frame_support::{ }; use sp_runtime::traits::{CheckedAdd, CheckedSub}; use sp_runtime::DispatchError; +use sp_runtime::Percent; +use tangle_primitives::BlueprintId; use tangle_primitives::ServiceManager; impl Pallet { @@ -298,4 +302,43 @@ impl Pallet { Ok(()) } + + pub fn slash_operator( + operator: &T::AccountId, + blueprint_id: BlueprintId, + percentage: Percent, + ) -> Result<(), DispatchError> { + Operators::::try_mutate(operator, |maybe_operator| { + let operator_data = maybe_operator.as_mut().ok_or(Error::::NotAnOperator)?; + ensure!(operator_data.status == OperatorStatus::Active, Error::::NotActiveOperator); + + // Slash operator stake + let amount = percentage.mul_floor(operator_data.stake); + operator_data.stake = operator_data + .stake + .checked_sub(&amount) + .ok_or(Error::::InsufficientStakeRemaining)?; + + // Slash each delegator + for delegator in operator_data.delegations.iter() { + // Ignore errors from individual delegator slashing + let _ = + Self::slash_delegator(&delegator.delegator, operator, blueprint_id, percentage); + } + + // transfer the slashed amount to the treasury + T::Currency::unreserve(operator, amount); + let _ = T::Currency::transfer( + operator, + &T::SlashedAmountRecipient::get(), + amount, + ExistenceRequirement::AllowDeath, + ); + + // emit event + Self::deposit_event(Event::OperatorSlashed { who: operator.clone(), amount }); + + Ok(()) + }) + } } diff --git a/pallets/multi-asset-delegation/src/lib.rs b/pallets/multi-asset-delegation/src/lib.rs index 31fa7dce..e6aa0748 100644 --- a/pallets/multi-asset-delegation/src/lib.rs +++ b/pallets/multi-asset-delegation/src/lib.rs @@ -90,6 +90,7 @@ pub mod pallet { use sp_runtime::traits::{MaybeSerializeDeserialize, Member, Zero}; use sp_std::vec::Vec; use sp_std::{collections::btree_map::BTreeMap, fmt::Debug, prelude::*}; + use tangle_primitives::BlueprintId; use tangle_primitives::{traits::ServiceManager, RoundIndex}; /// Configure the pallet by specifying the parameters and types on which it depends. @@ -184,6 +185,9 @@ pub mod pallet { /// The origin with privileged access type ForceOrigin: EnsureOrigin; + /// The address that receives slashed funds + type SlashedAmountRecipient: Get; + /// A type representing the weights required by the dispatchables of this pallet. type WeightInfo: crate::weights::WeightInfo; } @@ -303,6 +307,10 @@ pub mod pallet { asset_id: T::AssetId, action: AssetAction, }, + /// Operator has been slashed + OperatorSlashed { who: T::AccountId, amount: BalanceOf }, + /// Delegator has been slashed + DelegatorSlashed { who: T::AccountId, amount: BalanceOf }, } /// Errors emitted by the pallet. @@ -400,6 +408,8 @@ pub mod pallet { CapExceedsTotalSupply, /// An unstake request is already pending PendingUnstakeRequestExists, + /// The blueprint is not selected + BlueprintNotSelected, } /// Hooks for the pallet. @@ -743,7 +753,7 @@ pub mod pallet { /// Adds a blueprint ID to a delegator's selection. #[pallet::call_index(22)] #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] - pub fn add_blueprint_id(origin: OriginFor, blueprint_id: u32) -> DispatchResult { + pub fn add_blueprint_id(origin: OriginFor, blueprint_id: BlueprintId) -> DispatchResult { let who = ensure_signed(origin)?; let mut metadata = Self::delegators(&who).ok_or(Error::::NotDelegator)?; @@ -766,7 +776,10 @@ pub mod pallet { /// Removes a blueprint ID from a delegator's selection. #[pallet::call_index(23)] #[pallet::weight(Weight::from_parts(10_000, 0) + T::DbWeight::get().writes(1))] - pub fn remove_blueprint_id(origin: OriginFor, blueprint_id: u32) -> DispatchResult { + pub fn remove_blueprint_id( + origin: OriginFor, + blueprint_id: BlueprintId, + ) -> DispatchResult { let who = ensure_signed(origin)?; let mut metadata = Self::delegators(&who).ok_or(Error::::NotDelegator)?; diff --git a/pallets/multi-asset-delegation/src/mock.rs b/pallets/multi-asset-delegation/src/mock.rs index 321538e4..397a8989 100644 --- a/pallets/multi-asset-delegation/src/mock.rs +++ b/pallets/multi-asset-delegation/src/mock.rs @@ -122,6 +122,7 @@ parameter_types! { pub const MinOperatorBondAmount: u64 = 10_000; pub const BondDuration: u32 = 10; pub PID: PalletId = PalletId(*b"PotStake"); + pub const SlashedAmountRecipient : u64 = 0; #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] pub const MaxDelegatorBlueprints : u32 = 50; #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] @@ -155,6 +156,7 @@ impl pallet_multi_asset_delegation::Config for Test { type MaxWithdrawRequests = MaxWithdrawRequests; type MaxUnstakeRequests = MaxUnstakeRequests; type MaxDelegations = MaxDelegations; + type SlashedAmountRecipient = SlashedAmountRecipient; type WeightInfo = (); } diff --git a/pallets/multi-asset-delegation/src/tests/operator.rs b/pallets/multi-asset-delegation/src/tests/operator.rs index f8cb66f6..85f70f9a 100644 --- a/pallets/multi-asset-delegation/src/tests/operator.rs +++ b/pallets/multi-asset-delegation/src/tests/operator.rs @@ -14,8 +14,10 @@ // You should have received a copy of the GNU General Public License // along with Tangle. If not, see . use super::*; +use crate::types::DelegatorBlueprintSelection::Fixed; use crate::{types::OperatorStatus, CurrentRound, Error}; use frame_support::{assert_noop, assert_ok}; +use sp_runtime::Percent; #[test] fn join_operator_success() { @@ -509,3 +511,126 @@ fn go_online_success() { })); }); } + +#[test] +fn slash_operator_success() { + new_test_ext().execute_with(|| { + // Setup operator + let operator_stake = 10_000; + assert_ok!(MultiAssetDelegation::join_operators(RuntimeOrigin::signed(1), operator_stake)); + + // Setup delegators + let delegator_stake = 5_000; + let asset_id = 1; + let blueprint_id = 1; + + create_and_mint_tokens(asset_id, 2, delegator_stake); + mint_tokens(1, asset_id, 3, delegator_stake); + + // Setup delegator with fixed blueprint selection + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(2), + asset_id, + delegator_stake + )); + assert_ok!(MultiAssetDelegation::add_blueprint_id(RuntimeOrigin::signed(2), blueprint_id)); + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(2), + 1, + asset_id, + delegator_stake, + None + )); + + // Setup delegator with all blueprints + assert_ok!(MultiAssetDelegation::deposit( + RuntimeOrigin::signed(3), + asset_id, + delegator_stake + )); + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(3), + 1, + asset_id, + delegator_stake, + None + )); + + // Slash 50% of stakes + let slash_percentage = Percent::from_percent(50); + assert_ok!(MultiAssetDelegation::slash_operator(&1, blueprint_id, slash_percentage)); + + // Verify operator stake was slashed + let operator_info = MultiAssetDelegation::operator_info(1).unwrap(); + assert_eq!(operator_info.stake, operator_stake / 2); + + // Verify fixed delegator stake was slashed + let delegator_2 = MultiAssetDelegation::delegators(2).unwrap(); + let delegation_2 = delegator_2.delegations.iter().find(|d| d.operator == 1).unwrap(); + assert_eq!(delegation_2.amount, delegator_stake / 2); + + // Verify all-blueprints delegator stake was slashed + let delegator_3 = MultiAssetDelegation::delegators(3).unwrap(); + let delegation_3 = delegator_3.delegations.iter().find(|d| d.operator == 1).unwrap(); + assert_eq!(delegation_3.amount, delegator_stake / 2); + + // Verify event + System::assert_has_event(RuntimeEvent::MultiAssetDelegation(Event::OperatorSlashed { + who: 1, + amount: operator_stake / 2, + })); + }); +} + +#[test] +fn slash_operator_not_an_operator() { + new_test_ext().execute_with(|| { + assert_noop!( + MultiAssetDelegation::slash_operator(&1, 1, Percent::from_percent(50)), + Error::::NotAnOperator + ); + }); +} + +#[test] +fn slash_operator_not_active() { + new_test_ext().execute_with(|| { + // Setup and deactivate operator + assert_ok!(MultiAssetDelegation::join_operators(RuntimeOrigin::signed(1), 10_000)); + assert_ok!(MultiAssetDelegation::go_offline(RuntimeOrigin::signed(1))); + + assert_noop!( + MultiAssetDelegation::slash_operator(&1, 1, Percent::from_percent(50)), + Error::::NotActiveOperator + ); + }); +} + +#[test] +fn slash_delegator_fixed_blueprint_not_selected() { + new_test_ext().execute_with(|| { + // Setup operator + assert_ok!(MultiAssetDelegation::join_operators(RuntimeOrigin::signed(1), 10_000)); + + create_and_mint_tokens(1, 2, 10_000); + + // Setup delegator with fixed blueprint selection + assert_ok!(MultiAssetDelegation::deposit(RuntimeOrigin::signed(2), 1, 5_000)); + + assert_ok!(MultiAssetDelegation::add_blueprint_id(RuntimeOrigin::signed(2), 1)); + + assert_ok!(MultiAssetDelegation::delegate( + RuntimeOrigin::signed(2), + 1, + 1, + 5_000, + Some(Fixed(vec![2].try_into().unwrap())), + )); + + // Try to slash with unselected blueprint + assert_noop!( + MultiAssetDelegation::slash_delegator(&2, &1, 5, Percent::from_percent(50)), + Error::::BlueprintNotSelected + ); + }); +} diff --git a/pallets/multi-asset-delegation/src/traits.rs b/pallets/multi-asset-delegation/src/traits.rs index 4a6b5b9c..2375ece5 100644 --- a/pallets/multi-asset-delegation/src/traits.rs +++ b/pallets/multi-asset-delegation/src/traits.rs @@ -16,7 +16,9 @@ use super::*; use crate::types::{BalanceOf, OperatorStatus}; use sp_runtime::traits::Zero; +use sp_runtime::Percent; use sp_std::prelude::*; +use tangle_primitives::BlueprintId; use tangle_primitives::{traits::MultiAssetDelegationInfo, RoundIndex}; impl MultiAssetDelegationInfo> for crate::Pallet { @@ -63,4 +65,8 @@ impl MultiAssetDelegationInfo> for .collect() }) } + + fn slash_operator(operator: &T::AccountId, blueprint_id: BlueprintId, percentage: Percent) { + let _ = Pallet::::slash_operator(operator, blueprint_id, percentage); + } } diff --git a/pallets/multi-asset-delegation/src/types/delegator.rs b/pallets/multi-asset-delegation/src/types/delegator.rs index 0bcc1bb3..8ce30328 100644 --- a/pallets/multi-asset-delegation/src/types/delegator.rs +++ b/pallets/multi-asset-delegation/src/types/delegator.rs @@ -16,12 +16,13 @@ use super::*; use frame_support::{pallet_prelude::Get, BoundedVec}; +use tangle_primitives::BlueprintId; /// Represents how a delegator selects which blueprints to work with. #[derive(Clone, PartialEq, Encode, Decode, RuntimeDebug, TypeInfo, Default, Eq)] pub enum DelegatorBlueprintSelection> { /// The delegator works with a fixed set of blueprints. - Fixed(BoundedVec), + Fixed(BoundedVec), /// The delegator works with all available blueprints. #[default] All, diff --git a/pallets/services/src/mock.rs b/pallets/services/src/mock.rs index f54f0efd..919ff55f 100644 --- a/pallets/services/src/mock.rs +++ b/pallets/services/src/mock.rs @@ -280,6 +280,13 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo ) -> Vec<(AccountId, Balance, Self::AssetId)> { Default::default() } + + fn slash_operator( + _operator: &AccountId, + _blueprint_id: tangle_primitives::BlueprintId, + _percentage: sp_runtime::Percent, + ) { + } } parameter_types! { diff --git a/precompiles/multi-asset-delegation/src/mock.rs b/precompiles/multi-asset-delegation/src/mock.rs index 9698c68a..a999578d 100644 --- a/precompiles/multi-asset-delegation/src/mock.rs +++ b/precompiles/multi-asset-delegation/src/mock.rs @@ -334,6 +334,7 @@ parameter_types! { pub const MinOperatorBondAmount: u64 = 10_000; pub const BondDuration: u32 = 10; pub PID: PalletId = PalletId(*b"PotStake"); + pub SlashedAmountRecipient : AccountId = TestAccount::Alex.into(); #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] pub const MaxDelegatorBlueprints : u32 = 50; #[derive(PartialEq, Eq, Clone, Copy, Debug, Encode, Decode, MaxEncodedLen, TypeInfo)] @@ -366,6 +367,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type MaxWithdrawRequests = MaxWithdrawRequests; type MaxUnstakeRequests = MaxUnstakeRequests; type MaxDelegations = MaxDelegations; + type SlashedAmountRecipient = SlashedAmountRecipient; type PalletId = PID; type WeightInfo = (); } diff --git a/precompiles/services/src/mock.rs b/precompiles/services/src/mock.rs index 9a203e1e..57b0ede6 100644 --- a/precompiles/services/src/mock.rs +++ b/precompiles/services/src/mock.rs @@ -390,6 +390,13 @@ impl tangle_primitives::traits::MultiAssetDelegationInfo ) -> Vec<(AccountId, Balance, Self::AssetId)> { Default::default() } + + fn slash_operator( + _operator: &AccountId, + _blueprint_id: tangle_primitives::BlueprintId, + _percentage: sp_runtime::Percent, + ) { + } } parameter_types! { diff --git a/primitives/src/traits/multi_asset_delegation.rs b/primitives/src/traits/multi_asset_delegation.rs index f29e677c..73b0d900 100644 --- a/primitives/src/traits/multi_asset_delegation.rs +++ b/primitives/src/traits/multi_asset_delegation.rs @@ -99,4 +99,10 @@ pub trait MultiAssetDelegationInfo { fn get_delegators_for_operator( operator: &AccountId, ) -> Vec<(AccountId, Balance, Self::AssetId)>; + + fn slash_operator( + operator: &AccountId, + blueprint_id: crate::BlueprintId, + percentage: sp_runtime::Percent, + ); } diff --git a/runtime/mainnet/src/lib.rs b/runtime/mainnet/src/lib.rs index a11a7f8b..59b5818e 100644 --- a/runtime/mainnet/src/lib.rs +++ b/runtime/mainnet/src/lib.rs @@ -1267,6 +1267,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type ForceOrigin = frame_system::EnsureRoot; type PalletId = PID; type VaultId = AssetId; + type SlashedAmountRecipient = TreasuryAccount; type MaxDelegatorBlueprints = MaxDelegatorBlueprints; type MaxOperatorBlueprints = MaxOperatorBlueprints; type MaxWithdrawRequests = MaxWithdrawRequests; diff --git a/runtime/testnet/src/lib.rs b/runtime/testnet/src/lib.rs index 6f24c052..da36b14c 100644 --- a/runtime/testnet/src/lib.rs +++ b/runtime/testnet/src/lib.rs @@ -1484,6 +1484,7 @@ impl pallet_multi_asset_delegation::Config for Runtime { type MinDelegateAmount = MinDelegateAmount; type Fungibles = Assets; type AssetId = AssetId; + type SlashedAmountRecipient = TreasuryAccount; type ForceOrigin = frame_system::EnsureRoot; type PalletId = PID; type VaultId = AssetId;