Skip to content

Commit

Permalink
feat: Improve traits for slashing (#829)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
1xstj authored Nov 22, 2024
1 parent b50019f commit 967bbae
Show file tree
Hide file tree
Showing 13 changed files with 286 additions and 3 deletions.
69 changes: 69 additions & 0 deletions pallets/multi-asset-delegation/src/functions/delegate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,14 @@
// along with Tangle. If not, see <http://www.gnu.org/licenses/>.
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<T: Config> Pallet<T> {
/// Processes the delegation of an amount of an asset to an operator.
Expand Down Expand Up @@ -338,4 +343,68 @@ impl<T: Config> Pallet<T> {
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::<T>::try_mutate(delegator, |maybe_metadata| {
let metadata = maybe_metadata.as_mut().ok_or(Error::<T>::NotDelegator)?;

let delegation = metadata
.delegations
.iter_mut()
.find(|d| &d.operator == operator)
.ok_or(Error::<T>::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::<T>::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::<T>::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(())
})
}
}
43 changes: 43 additions & 0 deletions pallets/multi-asset-delegation/src/functions/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<T: Config> Pallet<T> {
Expand Down Expand Up @@ -298,4 +302,43 @@ impl<T: Config> Pallet<T> {

Ok(())
}

pub fn slash_operator(
operator: &T::AccountId,
blueprint_id: BlueprintId,
percentage: Percent,
) -> Result<(), DispatchError> {
Operators::<T>::try_mutate(operator, |maybe_operator| {
let operator_data = maybe_operator.as_mut().ok_or(Error::<T>::NotAnOperator)?;
ensure!(operator_data.status == OperatorStatus::Active, Error::<T>::NotActiveOperator);

// Slash operator stake
let amount = percentage.mul_floor(operator_data.stake);
operator_data.stake = operator_data
.stake
.checked_sub(&amount)
.ok_or(Error::<T>::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(())
})
}
}
17 changes: 15 additions & 2 deletions pallets/multi-asset-delegation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -184,6 +185,9 @@ pub mod pallet {
/// The origin with privileged access
type ForceOrigin: EnsureOrigin<Self::RuntimeOrigin>;

/// The address that receives slashed funds
type SlashedAmountRecipient: Get<Self::AccountId>;

/// A type representing the weights required by the dispatchables of this pallet.
type WeightInfo: crate::weights::WeightInfo;
}
Expand Down Expand Up @@ -303,6 +307,10 @@ pub mod pallet {
asset_id: T::AssetId,
action: AssetAction,
},
/// Operator has been slashed
OperatorSlashed { who: T::AccountId, amount: BalanceOf<T> },
/// Delegator has been slashed
DelegatorSlashed { who: T::AccountId, amount: BalanceOf<T> },
}

/// Errors emitted by the pallet.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<T>, blueprint_id: u32) -> DispatchResult {
pub fn add_blueprint_id(origin: OriginFor<T>, blueprint_id: BlueprintId) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut metadata = Self::delegators(&who).ok_or(Error::<T>::NotDelegator)?;

Expand All @@ -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<T>, blueprint_id: u32) -> DispatchResult {
pub fn remove_blueprint_id(
origin: OriginFor<T>,
blueprint_id: BlueprintId,
) -> DispatchResult {
let who = ensure_signed(origin)?;
let mut metadata = Self::delegators(&who).ok_or(Error::<T>::NotDelegator)?;

Expand Down
2 changes: 2 additions & 0 deletions pallets/multi-asset-delegation/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 = ();
}

Expand Down
125 changes: 125 additions & 0 deletions pallets/multi-asset-delegation/src/tests/operator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@
// You should have received a copy of the GNU General Public License
// along with Tangle. If not, see <http://www.gnu.org/licenses/>.
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() {
Expand Down Expand Up @@ -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::<Test>::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::<Test>::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::<Test>::BlueprintNotSelected
);
});
}
6 changes: 6 additions & 0 deletions pallets/multi-asset-delegation/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<T: crate::Config> MultiAssetDelegationInfo<T::AccountId, BalanceOf<T>> for crate::Pallet<T> {
Expand Down Expand Up @@ -63,4 +65,8 @@ impl<T: crate::Config> MultiAssetDelegationInfo<T::AccountId, BalanceOf<T>> for
.collect()
})
}

fn slash_operator(operator: &T::AccountId, blueprint_id: BlueprintId, percentage: Percent) {
let _ = Pallet::<T>::slash_operator(operator, blueprint_id, percentage);
}
}
3 changes: 2 additions & 1 deletion pallets/multi-asset-delegation/src/types/delegator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MaxBlueprints: Get<u32>> {
/// The delegator works with a fixed set of blueprints.
Fixed(BoundedVec<u32, MaxBlueprints>),
Fixed(BoundedVec<BlueprintId, MaxBlueprints>),
/// The delegator works with all available blueprints.
#[default]
All,
Expand Down
Loading

0 comments on commit 967bbae

Please sign in to comment.