From 5e2c3a4718a19bfd55e939c30c082276b216a932 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 + integration-tests/src/constants.rs | 20 +- integration-tests/src/tests/ct_migration.rs | 4 +- integration-tests/src/tests/e2e.rs | 14 +- .../src/tests/evaluator_slash_sideffects.rs | 226 ++++++++++++++++++ integration-tests/src/tests/mod.rs | 1 + 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 | 59 +++++ pallets/on-slash-vesting/src/mock.rs | 108 +++++++++ pallets/on-slash-vesting/src/test.rs | 88 +++++++ runtimes/polimec/Cargo.toml | 1 + runtimes/polimec/src/lib.rs | 1 + 17 files changed, 580 insertions(+), 14 deletions(-) create mode 100644 integration-tests/src/tests/evaluator_slash_sideffects.rs 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/integration-tests/src/constants.rs b/integration-tests/src/constants.rs index 47b16ca81..fcf5dcd36 100644 --- a/integration-tests/src/constants.rs +++ b/integration-tests/src/constants.rs @@ -378,12 +378,22 @@ pub mod polimec { const GENESIS_NUM_SELECTED_CANDIDATES: u32 = 5; #[allow(unused)] - pub fn set_prices() { + pub fn set_prices( + dot: Option, + usdc: Option, + usdt: Option, + plmc: Option, + ) { PolimecNet::execute_with(|| { - let dot = (AcceptedFundingAsset::DOT.id(), FixedU128::from_rational(69, 1)); - let usdc = (AcceptedFundingAsset::USDC.id(), FixedU128::from_rational(1, 1)); - let usdt = (AcceptedFundingAsset::USDT.id(), FixedU128::from_rational(1, 1)); - let plmc = (pallet_funding::PLMC_FOREIGN_ID, FixedU128::from_rational(840, 100)); + let dot_price = dot.unwrap_or(FixedU128::from_rational(69, 1)); + let usdc_price = usdc.unwrap_or(FixedU128::from_rational(1, 1)); + let usdt_price = usdt.unwrap_or(FixedU128::from_rational(1, 1)); + let plmc_price = plmc.unwrap_or(FixedU128::from_rational(840, 100)); + + let dot = (AcceptedFundingAsset::DOT.id(), dot_price); + let usdc = (AcceptedFundingAsset::USDC.id(), usdc_price); + let usdt = (AcceptedFundingAsset::USDT.id(), usdt_price); + let plmc = (pallet_funding::PLMC_FOREIGN_ID, plmc_price); let values: BoundedVec<(u32, FixedU128), ::MaxFeedValues> = vec![dot, usdc, usdt, plmc].try_into().expect("benchmarks can panic"); diff --git a/integration-tests/src/tests/ct_migration.rs b/integration-tests/src/tests/ct_migration.rs index 25ee151d2..11640d48b 100644 --- a/integration-tests/src/tests/ct_migration.rs +++ b/integration-tests/src/tests/ct_migration.rs @@ -190,7 +190,7 @@ fn create_settled_project() -> (ProjectId, Vec) { #[test] fn full_pallet_migration_test() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let (project_id, participants) = create_settled_project(); let _project_status = PolimecNet::execute_with(|| pallet_funding::ProjectsDetails::::get(project_id).unwrap().status); @@ -296,7 +296,7 @@ fn create_project_with_unsettled_participation(participation_type: Participation #[test] fn cannot_start_pallet_migration_with_unsettled_participations() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let tup_1 = create_project_with_unsettled_participation(ParticipationType::Evaluation); let tup_2 = create_project_with_unsettled_participation(ParticipationType::Bid); diff --git a/integration-tests/src/tests/e2e.rs b/integration-tests/src/tests/e2e.rs index 13fe4f4d0..790c24365 100644 --- a/integration-tests/src/tests/e2e.rs +++ b/integration-tests/src/tests/e2e.rs @@ -271,7 +271,7 @@ fn excel_ct_amounts() -> UserToCTBalance { #[test] fn evaluation_round_completed() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); @@ -286,7 +286,7 @@ fn evaluation_round_completed() { #[test] fn auction_round_completed() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); @@ -326,7 +326,7 @@ fn auction_round_completed() { #[test] fn community_round_completed() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); @@ -353,7 +353,7 @@ fn community_round_completed() { #[test] fn remainder_round_completed() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); @@ -386,7 +386,7 @@ fn remainder_round_completed() { #[test] fn funds_raised() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); @@ -418,7 +418,7 @@ fn funds_raised() { #[test] fn ct_minted() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); @@ -445,7 +445,7 @@ fn ct_minted() { #[test] fn ct_migrated() { - polimec::set_prices(); + polimec::set_prices(None, None, None, None); let mut inst = IntegrationInstantiator::new(None); diff --git a/integration-tests/src/tests/evaluator_slash_sideffects.rs b/integration-tests/src/tests/evaluator_slash_sideffects.rs new file mode 100644 index 000000000..ab291160c --- /dev/null +++ b/integration-tests/src/tests/evaluator_slash_sideffects.rs @@ -0,0 +1,226 @@ +use crate::{tests::defaults::*, *}; +use frame_support::{ + traits::{ + fungible::Mutate, + }, +}; +use frame_system::{pallet_prelude::BlockNumberFor, Account}; +use macros::generate_accounts; +use pallet_balances::AccountData; +use pallet_funding::*; +use pallet_vesting::VestingInfo; +use polimec_common::{USD_UNIT}; +use polimec_runtime::PLMC; +use sp_arithmetic::Perquintill; +use sp_runtime::{ + FixedU128, + MultiAddress::Id, +}; + +generate_accounts!(STASH, ALICE, BOB, CHARLIE, DAVE, ISSUER); + +#[test] +fn evaluator_slash_reduces_vesting_schedules() { + // set plmc price to 1 usd + polimec::set_prices(None, None, None, Some(FixedU128::from_float(1.0))); + + let mut inst = IntegrationInstantiator::new(None); + let alice: PolimecAccountId = ALICE.into(); + let bob: PolimecAccountId = BOB.into(); + + PolimecNet::execute_with(|| { + // Account that does the vested transfers (i.e. treasury) + PolimecBalances::set_balance(&STASH.into(), 1_000_000 * PLMC); + // For alice, we try to slash 4 schedules, each with a different duration and amount. + // One schedule should dissapear to due being fully vested before the slash, the other ones should be modified + PolimecBalances::set_balance(&alice.clone(), 0); + // For bob, we try to slash 1 schedule, which makes it dissapear since the slash amount is higher than the amount remaining for vesting + PolimecBalances::set_balance(&bob.clone(), BOB_STARTING_BALANCE); + + let slash_percent = ::EvaluatorSlash::get(); + + const BOB_STARTING_BALANCE: u128 = 100_000 * PLMC; + const LOCK_1: u128 = 10_000 * PLMC; + const LOCK_2: u128 = 13_000 * PLMC; + const LOCK_3: u128 = 7_500 * PLMC; + const LOCK_4: u128 = 20_000 * PLMC; + const PER_BLOCK_1: u128 = 10 * PLMC; + const PER_BLOCK_2: u128 = 650 * PLMC; + const PER_BLOCK_3: u128 = 5 * PLMC; + const PER_BLOCK_4: u128 = 200 * PLMC; + const DURATION_1: u128 = LOCK_1 / PER_BLOCK_1; + const DURATION_3: u128 = LOCK_3 / PER_BLOCK_3; + const DURATION_4: u128 = LOCK_4 / PER_BLOCK_4; + + // Duration 1000 blocks + let vesting_info_1 = VestingInfo::new(LOCK_1, PER_BLOCK_1, 5); + // Duration 20 blocks + let vesting_info_2 = VestingInfo::new(LOCK_2, PER_BLOCK_2, 5); + // Duration 1500 blocks + let vesting_info_3 = VestingInfo::new(LOCK_3, PER_BLOCK_3, 5); + // Duration 100 blocks + let vesting_info_4 = VestingInfo::new(LOCK_4, PER_BLOCK_4, 5); + + let total_alice_transferred = LOCK_1 + LOCK_2 + LOCK_3 + LOCK_4; + assert_ok!(PolimecVesting::vested_transfer( + PolimecOrigin::signed(STASH.into()), + Id(alice.clone()), + vesting_info_1 + )); + assert_ok!(PolimecVesting::vested_transfer( + PolimecOrigin::signed(STASH.into()), + Id(alice.clone()), + vesting_info_2 + )); + assert_ok!(PolimecVesting::vested_transfer( + PolimecOrigin::signed(STASH.into()), + Id(alice.clone()), + vesting_info_3 + )); + assert_ok!(PolimecVesting::vested_transfer( + PolimecOrigin::signed(STASH.into()), + Id(alice.clone()), + vesting_info_4 + )); + + let alice_evaluation = UserToUSDBalance::::new(alice.clone(), 35_000 * USD_UNIT); + let alice_plmc_evaluated = + inst.calculate_evaluation_plmc_spent(vec![alice_evaluation.clone()], false)[0].plmc_amount; + let alice_slashed = slash_percent * alice_plmc_evaluated; + + const BOB_EVALUATION: u128 = 60_000; + // We want the amount to be slashed to be higher than the amount remaining for vesting, after unlocking some tokens + let lock_5: u128 = ((slash_percent * BOB_EVALUATION) * PLMC) + PER_BLOCK_5 * 10; + const PER_BLOCK_5: u128 = 100 * PLMC; + let vesting_info_5 = VestingInfo::new(lock_5, PER_BLOCK_5, 5); + + assert_ok!(PolimecVesting::vested_transfer( + PolimecOrigin::signed(STASH.into()), + Id(bob.clone()), + vesting_info_5 + )); + let bob_evaluation = UserToUSDBalance::::new(bob.clone(), BOB_EVALUATION * USD_UNIT); + let bob_plmc_evaluated = + inst.calculate_evaluation_plmc_spent(vec![bob_evaluation.clone()], false)[0].plmc_amount; + let bob_slashed = slash_percent * bob_plmc_evaluated; + + // Set metadata so 50k USD succeeds the evaluation round + let mut project_metadata = default_project_metadata(ISSUER.into()); + project_metadata.total_allocation_size = 5_000 * CT_UNIT; + + // Create a project where alice and bob evaluated making the round successful, but then the project fails funding at block 25. + let project_id = inst.create_evaluating_project(project_metadata, ISSUER.into(), None); + assert_ok!(inst.evaluate_for_users(project_id, vec![alice_evaluation.clone(), bob_evaluation.clone()])); + assert_eq!(ProjectStatus::AuctionRound, inst.go_to_next_state(project_id)); + assert!(matches!(inst.go_to_next_state(project_id), ProjectStatus::CommunityRound(_))); + assert_eq!(ProjectStatus::FundingFailed, inst.go_to_next_state(project_id)); + assert_eq!(ProjectStatus::SettlementStarted(FundingOutcome::Failure), inst.go_to_next_state(project_id)); + assert_eq!(inst.current_block(), BlockNumberFor::::from(25u32)); + + // All schedules start at block 5, and funding ended at block 25 + const TIME_PASSED: u128 = 20u128; + + let alice_account_data = Account::::get(&alice.clone()).data; + assert_eq!( + alice_account_data, + AccountData { + free: total_alice_transferred - alice_plmc_evaluated, + reserved: alice_plmc_evaluated, + frozen: total_alice_transferred, + flags: Default::default(), + } + ); + assert_eq!(PolimecBalances::usable_balance(alice.clone()), 0); + + // vest schedule 2 was fully vested + assert_ok!(PolimecVesting::vest(PolimecOrigin::signed(alice.clone()))); + let alice_account_data = Account::::get(&alice.clone()).data; + let vested = (PER_BLOCK_1 + PER_BLOCK_2 + PER_BLOCK_3 + PER_BLOCK_4) * TIME_PASSED; + + let free = total_alice_transferred - alice_plmc_evaluated; + let reserved = alice_plmc_evaluated; + let frozen = total_alice_transferred - vested; + + // `untouchable` is the amount we need to substract from the free balance to get the usable balance. + // When the reserved amount is higher than the frozen amount, it means that the frozen balance restriction is fully covered by the already reserved tokens + // When the frozen amount is higher, it means we need to use some free tokens to cover this restriction. + // This amount can never be below the existential deposit. + let untouchable = frozen.saturating_sub(reserved).max(inst.get_ed()); + + assert_eq!(alice_account_data, AccountData { free, reserved, frozen, flags: Default::default() }); + assert_eq!(PolimecBalances::usable_balance(alice.clone()), free - untouchable); + + assert_ok!(PolimecFunding::settle_evaluation( + PolimecOrigin::signed(alice.clone()), + project_id, + alice.clone(), + 0 + )); + + let alice_schedules = >::get(alice.clone()).unwrap().to_vec(); + let new_lock_1 = LOCK_1 - (PER_BLOCK_1 * TIME_PASSED) - alice_slashed; + let new_lock_3 = LOCK_3 - (PER_BLOCK_3 * TIME_PASSED) - alice_slashed; + let new_lock_4 = LOCK_4 - (PER_BLOCK_4 * TIME_PASSED) - alice_slashed; + const TIME_REMAINING_1: u128 = DURATION_1 - TIME_PASSED; + const TIME_REMAINING_3: u128 = DURATION_3 - TIME_PASSED; + const TIME_REMAINING_4: u128 = DURATION_4 - TIME_PASSED; + let new_per_block_1 = new_lock_1 / TIME_REMAINING_1; + let new_per_block_3 = new_lock_3 / TIME_REMAINING_3; + let new_per_block_4 = new_lock_4 / TIME_REMAINING_4; + + let alice_account_data = Account::::get(&alice.clone()).data; + let free = free + alice_plmc_evaluated - alice_slashed; + let frozen = new_lock_1 + new_lock_3 + new_lock_4; + let reserved = 0u128; + let untouchable = frozen.saturating_sub(reserved).max(inst.get_ed()); + + assert_eq!( + alice_schedules, + vec![ + VestingInfo::new(new_lock_1, new_per_block_1, 25), + VestingInfo::new(new_lock_3, new_per_block_3, 25), + VestingInfo::new(new_lock_4, new_per_block_4, 25), + ] + ); + assert_eq!(alice_account_data, AccountData { free, reserved, frozen, flags: Default::default() }); + assert_eq!(PolimecBalances::usable_balance(alice.clone()), free - untouchable); + + let bob_account_data = Account::::get(&bob.clone()).data; + let free = BOB_STARTING_BALANCE + lock_5 - bob_plmc_evaluated; + let reserved = bob_plmc_evaluated; + let frozen = lock_5; + assert_eq!(bob_account_data, AccountData { free, reserved, frozen, flags: Default::default() }); + let untouchable = frozen.saturating_sub(reserved).max(inst.get_ed()); + assert_eq!(PolimecBalances::usable_balance(bob.clone()), free - untouchable); + + // Schedule has some tokens vested, and the remaining locked amount is lower than the slash about to occur + assert_ok!(PolimecVesting::vest(PolimecOrigin::signed(bob.clone()))); + let bob_account_data = Account::::get(&bob.clone()).data; + let vested = PER_BLOCK_5 * TIME_PASSED; + + let free = BOB_STARTING_BALANCE + lock_5 - bob_plmc_evaluated; + let reserved = bob_plmc_evaluated; + let frozen = lock_5 - vested; + let untouchable = frozen.saturating_sub(reserved).max(inst.get_ed()); + assert_eq!(bob_account_data, AccountData { free, reserved, frozen, flags: Default::default() }); + assert_eq!(PolimecBalances::usable_balance(bob.clone()), free - untouchable); + + // Here the slash amount is higher than the amount remaining for vesting, so the schedule should dissapear + assert_ok!(PolimecFunding::settle_evaluation(PolimecOrigin::signed(bob.clone()), project_id, bob.clone(), 1)); + assert!(bob_slashed > lock_5 - vested && bob_slashed < lock_5); + + assert!(>::get(bob.clone()).is_none()); + let bob_account_data = Account::::get(&bob.clone()).data; + let free = free + bob_plmc_evaluated - bob_slashed; + let frozen = 0u128; + let reserved = 0u128; + let untouchable = frozen.saturating_sub(reserved).max(inst.get_ed()); + + assert_eq!(bob_account_data, AccountData { free, reserved, frozen, flags: Default::default() }); + assert_close_enough!( + PolimecBalances::usable_balance(bob.clone()), + free - untouchable, + Perquintill::from_float(0.999) + ); + }); +} diff --git a/integration-tests/src/tests/mod.rs b/integration-tests/src/tests/mod.rs index 6453b6c40..d45335565 100644 --- a/integration-tests/src/tests/mod.rs +++ b/integration-tests/src/tests/mod.rs @@ -18,6 +18,7 @@ mod credentials; mod ct_migration; mod defaults; mod e2e; +mod evaluator_slash_sideffects; mod governance; mod oracle; mod reserve_backed_transfers; 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 09e16d2e0..c5c0b530b 100644 --- a/pallets/funding/src/lib.rs +++ b/pallets/funding/src/lib.rs @@ -156,6 +156,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}, @@ -361,6 +362,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..1aeea9c79 100644 --- a/pallets/funding/src/mock.rs +++ b/pallets/funding/src/mock.rs @@ -418,6 +418,7 @@ impl Config for TestRuntime { type MinUsdPerEvaluation = MinUsdPerEvaluation; type Multiplier = Multiplier; type NativeCurrency = Balances; + type OnSlash = (); type PalletId = FundingPalletId; type Price = FixedU128; type PriceProvider = ConstPriceProvider; 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..9e9c17ea6 --- /dev/null +++ b/pallets/on-slash-vesting/src/lib.rs @@ -0,0 +1,59 @@ +// 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 { + 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() { + 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..e29830ffc --- /dev/null +++ b/pallets/on-slash-vesting/src/test.rs @@ -0,0 +1,88 @@ +extern crate alloc; +use super::{mock::*, *}; +use frame_support::{ + assert_ok, + traits::tokens::fungible::{BalancedHold, Inspect, Mutate, MutateHold}, +}; +use mock::{Balances as PalletBalances, System as PalletSystem, Vesting as PalletVesting}; +use pallet_balances::AccountData; +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..953c3ed85 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;