From ff8170155d62351d322d7608a5b5d758b6340800 Mon Sep 17 00:00:00 2001 From: Juan Ignacio Rios Date: Wed, 4 Sep 2024 13:26:48 +0200 Subject: [PATCH] slash behavior on pallet vesting --- Cargo.lock | 19 +++ Cargo.toml | 2 + pallets/funding/Cargo.toml | 1 + pallets/funding/src/functions/6_settlement.rs | 3 + pallets/funding/src/lib.rs | 4 + pallets/funding/src/mock.rs | 1 + pallets/on-slash-vesting/Cargo.toml | 42 +++++++ pallets/on-slash-vesting/src/lib.rs | 61 ++++++++++ pallets/on-slash-vesting/src/mock.rs | 108 ++++++++++++++++++ pallets/on-slash-vesting/src/test.rs | 94 +++++++++++++++ runtimes/polimec/Cargo.toml | 1 + runtimes/polimec/src/lib.rs | 1 + 12 files changed, 337 insertions(+) create mode 100644 pallets/on-slash-vesting/Cargo.toml create mode 100644 pallets/on-slash-vesting/src/lib.rs create mode 100644 pallets/on-slash-vesting/src/mock.rs create mode 100644 pallets/on-slash-vesting/src/test.rs diff --git a/Cargo.lock b/Cargo.lock index d4d553822..6b74d6534 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6528,6 +6528,23 @@ dependencies = [ "asn1-rs", ] +[[package]] +name = "on-slash-vesting" +version = "0.8.0" +dependencies = [ + "frame-support", + "frame-system", + "impl-trait-for-tuples", + "log", + "pallet-balances", + "pallet-vesting", + "parity-scale-codec", + "scale-info", + "serde", + "sp-io", + "sp-runtime", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -7258,6 +7275,7 @@ dependencies = [ "itertools 0.11.0", "log", "macros", + "on-slash-vesting", "pallet-assets", "pallet-balances", "pallet-insecure-randomness-collective-flip", @@ -8772,6 +8790,7 @@ dependencies = [ "frame-try-runtime", "hex-literal", "log", + "on-slash-vesting", "orml-oracle", "pallet-assets", "pallet-aura", diff --git a/Cargo.toml b/Cargo.toml index 69a4d5a8f..8e93ee393 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -66,6 +66,7 @@ pallet-sandbox = { path = "pallets/sandbox", default-features = false } pallet-parachain-staking = { path = "pallets/parachain-staking", default-features = false } pallet-linear-release = { path = "pallets/linear-release", default-features = false } polimec-receiver = { path = "pallets/polimec-receiver", default-features = false } +on-slash-vesting = { path = "pallets/on-slash-vesting", default-features = false } # Internal macros macros = { path = "macros" } @@ -109,6 +110,7 @@ color-print = "0.3.5" xcm-emulator = { version = "0.12.0", default-features = false } # Substrate (with default disabled) +impl-trait-for-tuples = { version = "0.2.2", default-features = false } frame-benchmarking = { version = "35.0.0", default-features = false } frame-benchmarking-cli = { version = "39.0.0" } frame-executive = { version = "35.0.0", default-features = false } diff --git a/pallets/funding/Cargo.toml b/pallets/funding/Cargo.toml index c21ef635e..86165bff7 100644 --- a/pallets/funding/Cargo.toml +++ b/pallets/funding/Cargo.toml @@ -28,6 +28,7 @@ log.workspace = true variant_count = "1.1.0" pallet-linear-release.workspace = true +on-slash-vesting.workspace = true # Substrate dependencies frame-support.workspace = true diff --git a/pallets/funding/src/functions/6_settlement.rs b/pallets/funding/src/functions/6_settlement.rs index 69635a103..f558ffd8d 100644 --- a/pallets/funding/src/functions/6_settlement.rs +++ b/pallets/funding/src/functions/6_settlement.rs @@ -11,6 +11,7 @@ use frame_support::{ Get, }, }; +use on_slash_vesting::OnSlash; use polimec_common::{ migration_types::{MigrationInfo, MigrationOrigin, MigrationStatus, ParticipationType}, ReleaseSchedule, @@ -393,6 +394,8 @@ impl Pallet { Fortitude::Force, )?; + T::OnSlash::on_slash(&evaluation.evaluator, slashed_amount); + Ok(evaluation.current_plmc_bond.saturating_sub(slashed_amount)) } diff --git a/pallets/funding/src/lib.rs b/pallets/funding/src/lib.rs index 8d53f605d..986463be0 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -153,6 +153,7 @@ pub mod pallet { traits::{OnFinalize, OnIdle, OnInitialize}, }; use frame_system::pallet_prelude::*; + use on_slash_vesting::OnSlash; use sp_arithmetic::Percent; use sp_runtime::{ traits::{Convert, ConvertBack, Get}, @@ -358,6 +359,9 @@ pub mod pallet { /// Struct holding information about extrinsic weights type WeightInfo: weights::WeightInfo; + + /// Callbacks for dealing with an evaluator slash on other pallets + type OnSlash: OnSlash, Balance>; } #[pallet::storage] diff --git a/pallets/funding/src/mock.rs b/pallets/funding/src/mock.rs index 7afcbab3c..62f730c85 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -434,6 +434,7 @@ impl Config for TestRuntime { type StringLimit = ConstU32<64>; type VerifierPublicKey = VerifierPublicKey; type WeightInfo = weights::SubstrateWeight; + type OnSlash = (); } // Configure a mock runtime to test the pallet. diff --git a/pallets/on-slash-vesting/Cargo.toml b/pallets/on-slash-vesting/Cargo.toml new file mode 100644 index 000000000..ffb1e7745 --- /dev/null +++ b/pallets/on-slash-vesting/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "on-slash-vesting" +authors.workspace = true +documentation.workspace = true +edition.workspace = true +homepage.workspace = true +license-file.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true + +[dependencies] +pallet-vesting.workspace = true +impl-trait-for-tuples.workspace = true +frame-support.workspace = true +frame-system.workspace = true +pallet-balances.workspace = true +log.workspace = true +parity-scale-codec.workspace = true +scale-info.workspace = true +sp-runtime.workspace = true +sp-io.workspace = true +serde.workspace = true +[lints] +workspace = true + + +[features] +default = [ "std" ] + +std = [ + "pallet-vesting/std", + "frame-support/std", + "frame-system/std", + "pallet-balances/std", + "log/std", + "parity-scale-codec/std", + "scale-info/std", + "sp-runtime/std", + "sp-io/std", + "serde/std", +] \ No newline at end of file diff --git a/pallets/on-slash-vesting/src/lib.rs b/pallets/on-slash-vesting/src/lib.rs new file mode 100644 index 000000000..3b2da724d --- /dev/null +++ b/pallets/on-slash-vesting/src/lib.rs @@ -0,0 +1,61 @@ +// Ensure we're `no_std` when compiling for Wasm. +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod test; + +extern crate alloc; +use alloc::vec::Vec; +use frame_support::{ + sp_runtime::{traits::Convert, FixedPointNumber, FixedU128}, + traits::{Currency, OriginTrait}, +}; +use pallet_vesting::Vesting; +use sp_runtime::traits::BlockNumberProvider; + +pub trait OnSlash { + fn on_slash(account: &AccountId, amount: Balance); +} + +#[impl_trait_for_tuples::impl_for_tuples(30)] +impl OnSlash for Tuple { + fn on_slash(account: &AccountId, amount: Balance) { + for_tuples!( #( Tuple::on_slash(account, amount.clone()); )* ); + } +} + +type AccountIdOf = ::AccountId; +impl OnSlash, u128> for pallet_vesting::Pallet +where + T: pallet_vesting::Config, + T::Currency: Currency, Balance = u128>, +{ + fn on_slash(account: &AccountIdOf, slashed_amount: u128) { + let Some(vesting_schedules) = >::get(account) else { return }; + let vesting_schedules = vesting_schedules.to_vec(); + let mut new_vesting_schedules = Vec::new(); + let now = T::BlockNumberProvider::current_block_number(); + for schedule in vesting_schedules { + dbg!(schedule); + let total_locked = schedule.locked_at::(now).saturating_sub(slashed_amount); + let start_block: u128 = T::BlockNumberToBalance::convert(now); + let end_block: u128 = schedule.ending_block_as_balance::(); + let duration = end_block.saturating_sub(start_block); + let per_block = FixedU128::from_rational(total_locked, duration).saturating_mul_int(1u128); + let new_schedule = pallet_vesting::VestingInfo::new(total_locked, per_block, now); + if new_schedule.is_valid() { + dbg!(new_schedule); + new_vesting_schedules.push(new_schedule); + } + } + let Ok(new_vesting_schedules) = new_vesting_schedules.try_into() else { + log::error!("Failed to convert new vesting schedules into BoundedVec"); + return + }; + >::set(account, Some(new_vesting_schedules)); + let vest_result = >::vest(T::RuntimeOrigin::signed(account.clone())); + debug_assert!(vest_result.is_ok()); + } +} diff --git a/pallets/on-slash-vesting/src/mock.rs b/pallets/on-slash-vesting/src/mock.rs new file mode 100644 index 000000000..0e7aebb80 --- /dev/null +++ b/pallets/on-slash-vesting/src/mock.rs @@ -0,0 +1,108 @@ +use frame_support::{ + __private::RuntimeDebug, + derive_impl, parameter_types, + sp_runtime::{traits::IdentityLookup, BuildStorage}, + traits::{VariantCount, WithdrawReasons}, +}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_runtime::traits::{ConvertInto, Identity}; + +frame_support::construct_runtime!( + pub enum TestRuntime { + System: frame_system = 0, + Balances: pallet_balances = 1, + Vesting: pallet_vesting = 2, + } +); +type Block = frame_system::mocking::MockBlock; + +#[derive( + Encode, + Decode, + Copy, + Clone, + PartialEq, + Eq, + RuntimeDebug, + MaxEncodedLen, + TypeInfo, + Ord, + PartialOrd, + Serialize, + Deserialize, +)] +pub enum MockRuntimeHoldReason { + Reason, + Reason2, +} +impl VariantCount for MockRuntimeHoldReason { + const VARIANT_COUNT: u32 = 2; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl frame_system::Config for TestRuntime { + type AccountData = pallet_balances::AccountData; + type AccountId = u64; + type Block = Block; + type Lookup = IdentityLookup; +} + +parameter_types! { + pub const MinVestedTransfer: u64 = 10; + pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = + WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub static ExistentialDeposit: u128 = 10u128.pow(7); +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] +impl pallet_balances::Config for TestRuntime { + type AccountStore = System; + type Balance = u128; + type ExistentialDeposit = ExistentialDeposit; + type RuntimeHoldReason = MockRuntimeHoldReason; +} + +impl pallet_vesting::Config for TestRuntime { + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkReason = BenchmarkReason; + type BlockNumberProvider = System; + type BlockNumberToBalance = ConvertInto; + type Currency = Balances; + type MinVestedTransfer = MinVestedTransfer; + type RuntimeEvent = RuntimeEvent; + type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; + type WeightInfo = (); + + const MAX_VESTING_SCHEDULES: u32 = 6; +} + +#[derive(Default)] +pub struct ExtBuilder { + pub existential_deposit: u128, +} + +impl ExtBuilder { + pub fn existential_deposit(mut self, existential_deposit: u128) -> Self { + self.existential_deposit = existential_deposit; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + EXISTENTIAL_DEPOSIT.with(|v| *v.borrow_mut() = self.existential_deposit); + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![ + (1, self.existential_deposit), + (2, self.existential_deposit), + (3, self.existential_deposit), + (4, self.existential_deposit), + ], + } + .assimilate_storage(&mut t) + .unwrap(); + + sp_io::TestExternalities::new(t) + } +} diff --git a/pallets/on-slash-vesting/src/test.rs b/pallets/on-slash-vesting/src/test.rs new file mode 100644 index 000000000..4f6204ce7 --- /dev/null +++ b/pallets/on-slash-vesting/src/test.rs @@ -0,0 +1,94 @@ +extern crate alloc; +use super::{mock::*, *}; +use frame_support::{ + assert_ok, + traits::tokens::fungible::{BalancedHold, Inspect, Mutate, MutateHold}, +}; +use pallet_balances::AccountData; +use mock::{Balances as PalletBalances, System as PalletSystem, Vesting as PalletVesting}; +use pallet_vesting::VestingInfo; + +#[test] +fn one_schedule() { + ExtBuilder { existential_deposit: 1 }.build().execute_with(|| { + >::set_balance(&1, 0); + >::set_balance(&2, 100); + let vesting_info = VestingInfo::new(100, 10, 1); + assert_ok!(PalletVesting::vested_transfer(RuntimeOrigin::signed(2), 1, vesting_info)); + assert_ok!(>::hold(&MockRuntimeHoldReason::Reason, &1u64, 30u128)); + + assert_eq!(PalletBalances::usable_balance(1), 0); + + PalletSystem::set_block_number(3); + // Unlock 20 + assert_ok!(PalletVesting::vest(RuntimeOrigin::signed(1))); + assert_eq!(PalletBalances::usable_balance(1), 20); + dbg!(>::get(1)); + + // Slash 30 + >::slash(&MockRuntimeHoldReason::Reason, &1u64, 30u128); + >::on_slash(&1, 30); + + // After calling on_slash, the previously unlocked 20 should be available again + assert_eq!(PalletBalances::usable_balance(1), 20); + }); +} + +#[test] +fn multiple_schedules() { + ExtBuilder { existential_deposit: 1 }.build().execute_with(|| { + >::set_balance(&1, 0); + >::set_balance(&2, 100); + >::mint_into(&2, 130).unwrap(); + >::mint_into(&2, 75).unwrap(); + >::mint_into(&2, 200).unwrap(); + + // Duration 10 blocks + let vesting_info_1 = VestingInfo::new(100, 10, 1); + // Duration 2 blocks + let vesting_info_2 = VestingInfo::new(130, 65, 1); + // Duration 15 blocks + let vesting_info_3 = VestingInfo::new(75, 5, 1); + // Duration 10 blocks + let vesting_info_4 = VestingInfo::new(200, 20, 1); + + assert_ok!(PalletVesting::vested_transfer(RuntimeOrigin::signed(2), 1, vesting_info_1)); + assert_ok!(PalletVesting::vested_transfer(RuntimeOrigin::signed(2), 1, vesting_info_2)); + assert_ok!(PalletVesting::vested_transfer(RuntimeOrigin::signed(2), 1, vesting_info_3)); + assert_ok!(PalletVesting::vested_transfer(RuntimeOrigin::signed(2), 1, vesting_info_4)); + + assert_ok!(>::hold(&MockRuntimeHoldReason::Reason, &1u64, 100u128)); + assert_eq!(PalletBalances::usable_balance(1), 0); + // see account data + dbg!(PalletSystem::account(1).data); + + PalletSystem::set_block_number(3); + + // Unlock 10*2 + 65*2 + 5*2 + 20*2 = 200 + assert_ok!(PalletVesting::vest(RuntimeOrigin::signed(1))); + assert_eq!(PalletBalances::usable_balance(1), 200); + + >::slash(&MockRuntimeHoldReason::Reason, &1u64, 65u128); + >::on_slash(&1, 65); + + let schedules = >::get(1).unwrap().to_vec(); + + // One schedule was fully vested before the slash, the other got the full amount reduced after the slash + assert_eq!(schedules, vec![ + VestingInfo::new(15, 1, 3), + VestingInfo::new(95, 11, 3), + ]); + + assert_eq!(PalletSystem::account(1).data, AccountData { + free: 405, + reserved: 35, + frozen: 110, + flags: Default::default(), + }); + + // What part of the frozen restriction applies to the free balance after applying it to the slash + let untouchable = 110 - 35; + assert_eq!(PalletBalances::usable_balance(1), 405-untouchable); + + }); +} diff --git a/runtimes/polimec/Cargo.toml b/runtimes/polimec/Cargo.toml index acdfc9a5a..bfe2d8b38 100644 --- a/runtimes/polimec/Cargo.toml +++ b/runtimes/polimec/Cargo.toml @@ -37,6 +37,7 @@ pallet-linear-release.workspace = true shared-configuration.workspace = true polimec-common.workspace = true pallet-parachain-staking.workspace = true +on-slash-vesting.workspace = true # Substrate frame-benchmarking = { workspace = true, optional = true } diff --git a/runtimes/polimec/src/lib.rs b/runtimes/polimec/src/lib.rs index 03a22703d..abb8a7018 100644 --- a/runtimes/polimec/src/lib.rs +++ b/runtimes/polimec/src/lib.rs @@ -1057,6 +1057,7 @@ impl pallet_funding::Config for Runtime { type MinUsdPerEvaluation = MinUsdPerEvaluation; type Multiplier = pallet_funding::types::Multiplier; type NativeCurrency = Balances; + type OnSlash = (Vesting); type PalletId = FundingPalletId; type Price = Price; type PriceProvider = OraclePriceProvider;