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;