diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs index 7f080c9bb8b3c..3e885748db018 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -231,6 +231,7 @@ impl frame_system::Config for Runtime { type SingleBlockMigrations = Migrations; type OnNewAccount = pallet_revive::AutoMapper; type OnKilledAccount = pallet_revive::AutoMapper; + type BaseCallFilter = ValidatorVestingCallFilter; } impl cumulus_pallet_weight_reclaim::Config for Runtime { @@ -540,6 +541,8 @@ parameter_types! { pub const MinVestedTransfer: Balance = 100 * CENTS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -551,6 +554,33 @@ impl pallet_vesting::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = weights::pallet_vesting::WeightInfo; type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; + type LockId = VestingLockId; +} + +parameter_types! { + pub const ValidatorVestingLockId: frame_support::traits::LockIdentifier = *b"stkinctv"; +} + +/// Blocks direct user access to `vested_transfer` on the validator-incentive vesting instance. We +/// don't include `force_vested_transfer` because it requires root origin, which bypasses all +/// filters. +pub struct ValidatorVestingCallFilter; +impl frame_support::traits::Contains for ValidatorVestingCallFilter { + fn contains(call: &RuntimeCall) -> bool { + !matches!(call, RuntimeCall::ValidatorVesting(pallet_vesting::Call::vested_transfer { .. })) + } +} + +impl pallet_vesting::Config for Runtime { + const MAX_VESTING_SCHEDULES: u32 = 100; + type BlockNumberProvider = RelaychainDataProvider; + type BlockNumberToBalance = ConvertInto; + type Currency = Balances; + type MinVestedTransfer = MinVestedTransfer; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = weights::pallet_vesting::WeightInfo; + type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; + type LockId = ValidatorVestingLockId; } parameter_types! { @@ -1766,6 +1796,7 @@ construct_runtime!( AssetTxPayment: pallet_asset_conversion_tx_payment = 13, Vesting: pallet_vesting = 14, PgasAllowance: pallet_pgas_allowance = 15, + ValidatorVesting: pallet_vesting:: = 16, // Collator support. the order of these 5 are important and shall not change. Authorship: pallet_authorship = 20, @@ -2259,6 +2290,7 @@ mod benches { [cumulus_pallet_xcmp_queue, XcmpQueue] [pallet_treasury, Treasury] [pallet_vesting, Vesting] + [pallet_vesting, ValidatorVesting] [pallet_vesting_precompiles, VestingPrecompiles] [pallet_whitelist, Whitelist] [pallet_xcm_bridge_hub_router, ToRococo] diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs index 845dafa34bc43..d82189533ffc5 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/staking.rs @@ -274,6 +274,7 @@ parameter_types! { pub const MaxNominations: u32 = ::LIMIT as u32; pub const MaxEraDuration: u64 = RelaySessionDuration::get() as u64 * RELAY_CHAIN_SLOT_DURATION_MILLIS as u64 * SessionsPerEra::get() as u64; pub MaxPruningItems: u32 = 100; + pub const ValidatorIncentiveVestingDuration: BlockNumber = 365 * RC_DAYS; } impl pallet_staking_async::Config for Runtime { @@ -312,6 +313,12 @@ impl pallet_staking_async::Config for Runtime { pallet_staking_async::reward::DefaultStakerRewardCalculator; type MaxPruningItems = MaxPruningItems; type WeightInfo = weights::pallet_staking_async::WeightInfo; + type VestingDuration = ValidatorIncentiveVestingDuration; + type VestingBlockNumberProvider = RelaychainDataProvider; + type ValidatorIncentivePayout = pallet_staking_async::VestedIncentivePayout< + Balances, + pallet_vesting::Pallet, + >; } // Relay Chain session keys type for validating session keys on AssetHub. @@ -645,6 +652,7 @@ where #[cfg(test)] mod tests { use super::*; + use frame_support::traits::Contains; #[test] fn all_epmb_weights_sane() { @@ -657,4 +665,87 @@ mod tests { ); }) } + + #[test] + fn validator_vesting_call_filter_blocks_vested_transfer() { + // Permisionless `vested_transfer` is not allowed on the ValidatorVesting instance. + let call = RuntimeCall::ValidatorVesting(pallet_vesting::Call::vested_transfer { + target: AccountId::from([42u8; 32]).into(), + schedule: pallet_vesting::VestingInfo::new(MinVestedTransfer::get(), 1, 0), + }); + assert!(!ValidatorVestingCallFilter::contains(&call)); + } + + #[test] + fn validator_vesting_call_filter_allows_force_vested_transfer() { + // Since `force_vested_transfer` is root-only, it should always be accessible since + // it bypasses all filters. + let call = RuntimeCall::ValidatorVesting(pallet_vesting::Call::force_vested_transfer { + source: AccountId::from([1u8; 32]).into(), + target: AccountId::from([2u8; 32]).into(), + schedule: pallet_vesting::VestingInfo::new(MinVestedTransfer::get(), 1, 0), + }); + assert!(ValidatorVestingCallFilter::contains(&call)); + } + + #[test] + fn validator_vesting_call_filter_allows_user_facing_calls() { + // We must keep `vest` and `vest_other` open so holders can unlock their funds. + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::ValidatorVesting( + pallet_vesting::Call::vest {} + ))); + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::ValidatorVesting( + pallet_vesting::Call::vest_other { target: AccountId::from([1u8; 32]).into() } + ))); + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::ValidatorVesting( + pallet_vesting::Call::merge_schedules { schedule1_index: 0, schedule2_index: 1 } + ))); + // An unrelated pallet must also pass through. + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::Timestamp( + pallet_timestamp::Call::set { now: 0 } + ))); + } + + #[test] + fn add_to_vesting_works_bypassing_call_filter() { + // Since `add_to_vesting` is a plain internal Rust call (and not a dispatchable) it is + // always allowed as it does not go through filtering. + use frame_support::traits::tokens::VestedPayout; + sp_io::TestExternalities::default().execute_with(|| { + let source = AccountId::from([1u8; 32]); + let dest = AccountId::from([2u8; 32]); + let amount = MinVestedTransfer::get(); + + frame_support::assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + source.clone().into(), + amount + ExistentialDeposit::get(), + )); + + frame_support::assert_ok!( + as VestedPayout< + AccountId, + Balance, + >>::add_to_vesting(&source, &dest, amount, 20u32, 1u32,) + ); + + assert!( + pallet_vesting::Vesting::::get(&dest).is_some() + ); + }); + } + + #[test] + fn validator_vesting_call_filter_is_the_base_call_filter() { + // Verify that the runtime's BaseCallFilter is our filter, not `Everything`. This + // ensures the dispatchable-blocking is actually wired into the extrinsic pipeline. + let blocked = RuntimeCall::ValidatorVesting(pallet_vesting::Call::vested_transfer { + target: AccountId::from([0u8; 32]).into(), + schedule: pallet_vesting::VestingInfo::new(MinVestedTransfer::get(), 1, 0), + }); + assert!( + !::BaseCallFilter::contains(&blocked), + "BaseCallFilter must block ValidatorVesting::vested_transfer" + ); + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting.rs index 80ec555c54f6a..4d93348612abd 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting.rs @@ -262,4 +262,12 @@ impl pallet_vesting::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_create(_l: u32, _s: u32) -> Weight { + Weight::zero() + } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_merge(_l: u32, _s: u32) -> Weight { + Weight::zero() + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting_validator_vesting.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting_validator_vesting.rs new file mode 100644 index 0000000000000..bd0302cfce09b --- /dev/null +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting_validator_vesting.rs @@ -0,0 +1,375 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_vesting` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2026-05-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `9f9f0c9e67c2`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --extrinsic=* +// --runtime=target/production/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm +// --pallet=pallet_vesting +// --header=/__w/polkadot-sdk/polkadot-sdk/cumulus/file_header.txt +// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights +// --wasm-execution=compiled +// --steps=50 +// --repeat=20 +// --heap-pages=4096 +// --no-storage-info +// --no-min-squares +// --no-median-slopes + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_vesting`. +pub struct WeightInfo(PhantomData); +impl pallet_vesting::WeightInfo for WeightInfo { + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `301 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 41_630_000 picoseconds. + Weight::from_parts(40_955_877, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_226 + .saturating_add(Weight::from_parts(38_924, 0).saturating_mul(l.into())) + // Standard Error: 1_593 + .saturating_add(Weight::from_parts(99_092, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `301 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 44_610_000 picoseconds. + Weight::from_parts(45_036_710, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_272 + .saturating_add(Weight::from_parts(22_244, 0).saturating_mul(l.into())) + // Standard Error: 1_616 + .saturating_add(Weight::from_parts(81_576, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_other_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1641 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 48_315_000 picoseconds. + Weight::from_parts(46_022_657, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_724 + .saturating_add(Weight::from_parts(84_479, 0).saturating_mul(l.into())) + // Standard Error: 1_839 + .saturating_add(Weight::from_parts(125_204, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1641 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 51_038_000 picoseconds. + Weight::from_parts(52_687_138, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_993 + .saturating_add(Weight::from_parts(22_582, 0).saturating_mul(l.into())) + // Standard Error: 1_972 + .saturating_add(Weight::from_parts(95_023, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + fn vested_transfer(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1712 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 92_624_000 picoseconds. + Weight::from_parts(95_384_027, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 5_007 + .saturating_add(Weight::from_parts(23_275, 0).saturating_mul(l.into())) + // Standard Error: 2_473 + .saturating_add(Weight::from_parts(127_354, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + fn force_vested_transfer(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3052 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 101_838_000 picoseconds. + Weight::from_parts(104_455_223, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 5_525 + .saturating_add(Weight::from_parts(19_273, 0).saturating_mul(l.into())) + // Standard Error: 2_729 + .saturating_add(Weight::from_parts(125_295, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `303 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 42_577_000 picoseconds. + Weight::from_parts(40_829_414, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_362 + .saturating_add(Weight::from_parts(68_336, 0).saturating_mul(l.into())) + // Standard Error: 1_681 + .saturating_add(Weight::from_parts(104_699, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `303 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 45_462_000 picoseconds. + Weight::from_parts(45_255_829, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_754 + .saturating_add(Weight::from_parts(25_705, 0).saturating_mul(l.into())) + // Standard Error: 1_877 + .saturating_add(Weight::from_parts(111_073, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + fn force_remove_vesting_schedule(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1714 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 54_356_000 picoseconds. + Weight::from_parts(54_009_092, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 4_085 + .saturating_add(Weight::from_parts(50_291, 0).saturating_mul(l.into())) + // Standard Error: 2_042 + .saturating_add(Weight::from_parts(116_333, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + fn add_to_vesting_create(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2920 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 96_551_000 picoseconds. + Weight::from_parts(98_042_820, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 5_468 + .saturating_add(Weight::from_parts(45_177, 0).saturating_mul(l.into())) + // Standard Error: 2_701 + .saturating_add(Weight::from_parts(129_178, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `ValidatorVesting::Vesting` (r:1 w:1) + /// Proof: `ValidatorVesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn add_to_vesting_merge(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `2920 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 90_957_000 picoseconds. + Weight::from_parts(92_185_016, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 4_576 + .saturating_add(Weight::from_parts(42_358, 0).saturating_mul(l.into())) + // Standard Error: 2_260 + .saturating_add(Weight::from_parts(101_570, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } +} diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting_vesting.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting_vesting.rs new file mode 100644 index 0000000000000..9fd6d1de18028 --- /dev/null +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_vesting_vesting.rs @@ -0,0 +1,375 @@ +// Copyright (C) Parity Technologies (UK) Ltd. +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Autogenerated weights for `pallet_vesting` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 +//! DATE: 2026-05-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `9f9f0c9e67c2`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 + +// Executed Command: +// frame-omni-bencher +// v1 +// benchmark +// pallet +// --extrinsic=* +// --runtime=target/production/wbuild/asset-hub-westend-runtime/asset_hub_westend_runtime.wasm +// --pallet=pallet_vesting +// --header=/__w/polkadot-sdk/polkadot-sdk/cumulus/file_header.txt +// --output=./cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights +// --wasm-execution=compiled +// --steps=50 +// --repeat=20 +// --heap-pages=4096 +// --no-storage-info +// --no-min-squares +// --no-median-slopes + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::Weight}; +use core::marker::PhantomData; + +/// Weight functions for `pallet_vesting`. +pub struct WeightInfo(PhantomData); +impl pallet_vesting::WeightInfo for WeightInfo { + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `471 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 42_499_000 picoseconds. + Weight::from_parts(43_029_373, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_155 + .saturating_add(Weight::from_parts(26_182, 0).saturating_mul(l.into())) + // Standard Error: 1_558 + .saturating_add(Weight::from_parts(92_920, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `471 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 45_159_000 picoseconds. + Weight::from_parts(43_329_313, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 4_032 + .saturating_add(Weight::from_parts(59_023, 0).saturating_mul(l.into())) + // Standard Error: 1_991 + .saturating_add(Weight::from_parts(101_258, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_other_locked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1811 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 49_794_000 picoseconds. + Weight::from_parts(48_101_416, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 4_785 + .saturating_add(Weight::from_parts(61_423, 0).saturating_mul(l.into())) + // Standard Error: 2_363 + .saturating_add(Weight::from_parts(123_927, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn vest_other_unlocked(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1811 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 52_637_000 picoseconds. + Weight::from_parts(52_718_587, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_378 + .saturating_add(Weight::from_parts(34_923, 0).saturating_mul(l.into())) + // Standard Error: 1_668 + .saturating_add(Weight::from_parts(90_614, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + fn vested_transfer(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1882 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 94_785_000 picoseconds. + Weight::from_parts(97_465_404, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 5_844 + .saturating_add(Weight::from_parts(14_368, 0).saturating_mul(l.into())) + // Standard Error: 2_886 + .saturating_add(Weight::from_parts(133_116, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + fn force_vested_transfer(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3222 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 103_499_000 picoseconds. + Weight::from_parts(104_790_160, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 6_190 + .saturating_add(Weight::from_parts(45_776, 0).saturating_mul(l.into())) + // Standard Error: 3_057 + .saturating_add(Weight::from_parts(131_920, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `473 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 43_858_000 picoseconds. + Weight::from_parts(43_401_328, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_091 + .saturating_add(Weight::from_parts(36_851, 0).saturating_mul(l.into())) + // Standard Error: 1_545 + .saturating_add(Weight::from_parts(99_690, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `473 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 46_791_000 picoseconds. + Weight::from_parts(45_715_914, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 2_827 + .saturating_add(Weight::from_parts(51_612, 0).saturating_mul(l.into())) + // Standard Error: 1_413 + .saturating_add(Weight::from_parts(104_035, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[2, 100]`. + fn force_remove_vesting_schedule(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `1884 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 56_040_000 picoseconds. + Weight::from_parts(56_225_789, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 3_778 + .saturating_add(Weight::from_parts(41_951, 0).saturating_mul(l.into())) + // Standard Error: 1_889 + .saturating_add(Weight::from_parts(103_143, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(3)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 99]`. + fn add_to_vesting_create(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3090 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 97_350_000 picoseconds. + Weight::from_parts(101_287_863, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 4_593 + .saturating_add(Weight::from_parts(8_744, 0).saturating_mul(l.into())) + // Standard Error: 2_268 + .saturating_add(Weight::from_parts(119_770, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } + /// Storage: `ParachainSystem::ValidationData` (r:1 w:0) + /// Proof: `ParachainSystem::ValidationData` (`max_values`: Some(1), `max_size`: None, mode: `Measured`) + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(3650), added: 6125, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(103), added: 2578, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 100]`. + fn add_to_vesting_merge(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `3090 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `7115 + l * (25 ±0) + s * (36 ±0)` + // Minimum execution time: 93_160_000 picoseconds. + Weight::from_parts(93_022_055, 0) + .saturating_add(Weight::from_parts(0, 7115)) + // Standard Error: 4_322 + .saturating_add(Weight::from_parts(64_980, 0).saturating_mul(l.into())) + // Standard Error: 2_134 + .saturating_add(Weight::from_parts(91_727, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(6)) + .saturating_add(T::DbWeight::get().writes(4)) + .saturating_add(Weight::from_parts(0, 25).saturating_mul(l.into())) + .saturating_add(Weight::from_parts(0, 36).saturating_mul(s.into())) + } +} diff --git a/polkadot/runtime/common/src/claims/mock.rs b/polkadot/runtime/common/src/claims/mock.rs index 640df6ec6a8ab..1bd04a33aa548 100644 --- a/polkadot/runtime/common/src/claims/mock.rs +++ b/polkadot/runtime/common/src/claims/mock.rs @@ -58,6 +58,8 @@ parameter_types! { pub const MinVestedTransfer: u64 = 1; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Test { @@ -68,6 +70,7 @@ impl pallet_vesting::Config for Test { type WeightInfo = (); type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; const MAX_VESTING_SCHEDULES: u32 = 28; } diff --git a/polkadot/runtime/common/src/purchase/mock.rs b/polkadot/runtime/common/src/purchase/mock.rs index ec8599f3b792b..58cd66572f8cb 100644 --- a/polkadot/runtime/common/src/purchase/mock.rs +++ b/polkadot/runtime/common/src/purchase/mock.rs @@ -82,6 +82,8 @@ parameter_types! { pub const MinVestedTransfer: u64 = 1; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Test { @@ -92,6 +94,7 @@ impl pallet_vesting::Config for Test { type WeightInfo = (); type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; const MAX_VESTING_SCHEDULES: u32 = 28; } diff --git a/polkadot/runtime/rococo/src/lib.rs b/polkadot/runtime/rococo/src/lib.rs index fa5bdb969b49e..4c9a64977883f 100644 --- a/polkadot/runtime/rococo/src/lib.rs +++ b/polkadot/runtime/rococo/src/lib.rs @@ -869,6 +869,8 @@ parameter_types! { pub const MinVestedTransfer: Balance = 100 * CENTS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -879,6 +881,7 @@ impl pallet_vesting::Config for Runtime { type WeightInfo = weights::pallet_vesting::WeightInfo; type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; const MAX_VESTING_SCHEDULES: u32 = 28; } diff --git a/polkadot/runtime/rococo/src/weights/pallet_vesting.rs b/polkadot/runtime/rococo/src/weights/pallet_vesting.rs index 70eba81750fe6..1b95fe2a7103d 100644 --- a/polkadot/runtime/rococo/src/weights/pallet_vesting.rs +++ b/polkadot/runtime/rococo/src/weights/pallet_vesting.rs @@ -259,4 +259,12 @@ impl pallet_vesting::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_create(_l: u32, _s: u32) -> Weight { + Weight::zero() + } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_merge(_l: u32, _s: u32) -> Weight { + Weight::zero() + } } diff --git a/polkadot/runtime/test-runtime/src/lib.rs b/polkadot/runtime/test-runtime/src/lib.rs index 42ecff2f8eddd..385078fa5d18f 100644 --- a/polkadot/runtime/test-runtime/src/lib.rs +++ b/polkadot/runtime/test-runtime/src/lib.rs @@ -531,6 +531,8 @@ parameter_types! { pub storage MinVestedTransfer: Balance = 100 * DOLLARS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -541,6 +543,7 @@ impl pallet_vesting::Config for Runtime { type WeightInfo = (); type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; const MAX_VESTING_SCHEDULES: u32 = 28; } diff --git a/polkadot/runtime/westend/src/lib.rs b/polkadot/runtime/westend/src/lib.rs index 2f5aa051fa65d..6e73a47188416 100644 --- a/polkadot/runtime/westend/src/lib.rs +++ b/polkadot/runtime/westend/src/lib.rs @@ -1153,6 +1153,8 @@ parameter_types! { pub const MinVestedTransfer: Balance = 100 * CENTS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -1163,6 +1165,7 @@ impl pallet_vesting::Config for Runtime { type WeightInfo = weights::pallet_vesting::WeightInfo; type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; const MAX_VESTING_SCHEDULES: u32 = 28; } diff --git a/polkadot/runtime/westend/src/weights/pallet_vesting.rs b/polkadot/runtime/westend/src/weights/pallet_vesting.rs index b84cd313b6159..c9f4a74d02424 100644 --- a/polkadot/runtime/westend/src/weights/pallet_vesting.rs +++ b/polkadot/runtime/westend/src/weights/pallet_vesting.rs @@ -17,9 +17,9 @@ //! Autogenerated weights for `pallet_vesting` //! //! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 32.0.0 -//! DATE: 2025-02-21, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! DATE: 2026-05-27, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` //! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `3a2e9ae8a8f5`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` +//! HOSTNAME: `1439125013e9`, CPU: `Intel(R) Xeon(R) CPU @ 2.60GHz` //! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: 1024 // Executed Command: @@ -63,13 +63,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `345 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 38_225_000 picoseconds. - Weight::from_parts(37_860_470, 0) + // Minimum execution time: 36_104_000 picoseconds. + Weight::from_parts(36_459_298, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 1_479 - .saturating_add(Weight::from_parts(41_149, 0).saturating_mul(l.into())) - // Standard Error: 2_631 - .saturating_add(Weight::from_parts(76_064, 0).saturating_mul(s.into())) + // Standard Error: 678 + .saturating_add(Weight::from_parts(36_680, 0).saturating_mul(l.into())) + // Standard Error: 1_207 + .saturating_add(Weight::from_parts(72_193, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -85,13 +85,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `345 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 40_682_000 picoseconds. - Weight::from_parts(40_558_815, 0) + // Minimum execution time: 38_722_000 picoseconds. + Weight::from_parts(38_919_091, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 1_473 - .saturating_add(Weight::from_parts(35_138, 0).saturating_mul(l.into())) - // Standard Error: 2_620 - .saturating_add(Weight::from_parts(72_425, 0).saturating_mul(s.into())) + // Standard Error: 654 + .saturating_add(Weight::from_parts(29_277, 0).saturating_mul(l.into())) + // Standard Error: 1_164 + .saturating_add(Weight::from_parts(62_166, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -109,13 +109,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `448 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 40_813_000 picoseconds. - Weight::from_parts(40_248_990, 0) + // Minimum execution time: 38_787_000 picoseconds. + Weight::from_parts(38_650_405, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 1_925 - .saturating_add(Weight::from_parts(47_778, 0).saturating_mul(l.into())) - // Standard Error: 3_425 - .saturating_add(Weight::from_parts(88_421, 0).saturating_mul(s.into())) + // Standard Error: 605 + .saturating_add(Weight::from_parts(40_421, 0).saturating_mul(l.into())) + // Standard Error: 1_077 + .saturating_add(Weight::from_parts(69_087, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -133,13 +133,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `448 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 43_330_000 picoseconds. - Weight::from_parts(43_588_745, 0) + // Minimum execution time: 40_680_000 picoseconds. + Weight::from_parts(41_086_677, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 2_075 - .saturating_add(Weight::from_parts(35_838, 0).saturating_mul(l.into())) - // Standard Error: 3_693 - .saturating_add(Weight::from_parts(73_951, 0).saturating_mul(s.into())) + // Standard Error: 592 + .saturating_add(Weight::from_parts(32_081, 0).saturating_mul(l.into())) + // Standard Error: 1_054 + .saturating_add(Weight::from_parts(65_452, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -157,13 +157,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `519 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 80_026_000 picoseconds. - Weight::from_parts(82_148_674, 0) + // Minimum execution time: 80_462_000 picoseconds. + Weight::from_parts(81_938_697, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 3_243 - .saturating_add(Weight::from_parts(30_866, 0).saturating_mul(l.into())) - // Standard Error: 5_770 - .saturating_add(Weight::from_parts(99_755, 0).saturating_mul(s.into())) + // Standard Error: 1_141 + .saturating_add(Weight::from_parts(47_432, 0).saturating_mul(l.into())) + // Standard Error: 2_030 + .saturating_add(Weight::from_parts(105_282, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } @@ -181,13 +181,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `622 + l * (25 ±0) + s * (36 ±0)` // Estimated: `6196` - // Minimum execution time: 81_979_000 picoseconds. - Weight::from_parts(83_373_383, 0) + // Minimum execution time: 82_264_000 picoseconds. + Weight::from_parts(84_131_116, 0) .saturating_add(Weight::from_parts(0, 6196)) - // Standard Error: 3_069 - .saturating_add(Weight::from_parts(49_002, 0).saturating_mul(l.into())) - // Standard Error: 5_460 - .saturating_add(Weight::from_parts(105_265, 0).saturating_mul(s.into())) + // Standard Error: 1_131 + .saturating_add(Weight::from_parts(41_116, 0).saturating_mul(l.into())) + // Standard Error: 2_013 + .saturating_add(Weight::from_parts(97_954, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(5)) .saturating_add(T::DbWeight::get().writes(4)) } @@ -203,13 +203,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `345 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 39_190_000 picoseconds. - Weight::from_parts(38_673_517, 0) + // Minimum execution time: 37_054_000 picoseconds. + Weight::from_parts(37_171_633, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 1_789 - .saturating_add(Weight::from_parts(38_146, 0).saturating_mul(l.into())) - // Standard Error: 3_305 - .saturating_add(Weight::from_parts(97_870, 0).saturating_mul(s.into())) + // Standard Error: 591 + .saturating_add(Weight::from_parts(34_575, 0).saturating_mul(l.into())) + // Standard Error: 1_092 + .saturating_add(Weight::from_parts(65_112, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -225,13 +225,13 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `345 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 42_229_000 picoseconds. - Weight::from_parts(42_040_081, 0) + // Minimum execution time: 39_704_000 picoseconds. + Weight::from_parts(39_428_931, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 1_659 - .saturating_add(Weight::from_parts(38_531, 0).saturating_mul(l.into())) - // Standard Error: 3_065 - .saturating_add(Weight::from_parts(76_527, 0).saturating_mul(s.into())) + // Standard Error: 654 + .saturating_add(Weight::from_parts(37_772, 0).saturating_mul(l.into())) + // Standard Error: 1_209 + .saturating_add(Weight::from_parts(75_112, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(3)) .saturating_add(T::DbWeight::get().writes(2)) } @@ -249,14 +249,62 @@ impl pallet_vesting::WeightInfo for WeightInfo { // Proof Size summary in bytes: // Measured: `519 + l * (25 ±0) + s * (36 ±0)` // Estimated: `4764` - // Minimum execution time: 46_474_000 picoseconds. - Weight::from_parts(46_105_020, 0) + // Minimum execution time: 44_027_000 picoseconds. + Weight::from_parts(43_542_571, 0) .saturating_add(Weight::from_parts(0, 4764)) - // Standard Error: 1_706 - .saturating_add(Weight::from_parts(39_879, 0).saturating_mul(l.into())) - // Standard Error: 3_151 - .saturating_add(Weight::from_parts(87_824, 0).saturating_mul(s.into())) + // Standard Error: 830 + .saturating_add(Weight::from_parts(43_364, 0).saturating_mul(l.into())) + // Standard Error: 1_532 + .saturating_add(Weight::from_parts(86_933, 0).saturating_mul(s.into())) .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[0, 27]`. + fn add_to_vesting_create(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `622 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `6196` + // Minimum execution time: 79_181_000 picoseconds. + Weight::from_parts(80_987_537, 0) + .saturating_add(Weight::from_parts(0, 6196)) + // Standard Error: 1_071 + .saturating_add(Weight::from_parts(36_318, 0).saturating_mul(l.into())) + // Standard Error: 1_905 + .saturating_add(Weight::from_parts(93_931, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:2 w:2) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Balances::Locks` (r:1 w:1) + /// Proof: `Balances::Locks` (`max_values`: None, `max_size`: Some(1299), added: 3774, mode: `MaxEncodedLen`) + /// Storage: `Balances::Freezes` (r:1 w:0) + /// Proof: `Balances::Freezes` (`max_values`: None, `max_size`: Some(67), added: 2542, mode: `MaxEncodedLen`) + /// The range of component `l` is `[0, 49]`. + /// The range of component `s` is `[1, 28]`. + fn add_to_vesting_merge(l: u32, s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `622 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `6196` + // Minimum execution time: 74_699_000 picoseconds. + Weight::from_parts(75_019_617, 0) + .saturating_add(Weight::from_parts(0, 6196)) + // Standard Error: 919 + .saturating_add(Weight::from_parts(38_529, 0).saturating_mul(l.into())) + // Standard Error: 1_636 + .saturating_add(Weight::from_parts(69_985, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5)) + .saturating_add(T::DbWeight::get().writes(4)) + } } diff --git a/prdoc/pr_11xxx.prdoc b/prdoc/pr_11xxx.prdoc new file mode 100644 index 0000000000000..68b9e070b6dd2 --- /dev/null +++ b/prdoc/pr_11xxx.prdoc @@ -0,0 +1,116 @@ +title: 'staking-async: vesting-based validator self-stake incentive payouts' + +doc: +- audience: Runtime Dev + description: |- + Implements the vesting delivery mechanism for the validator self-stake incentive introduced + in issue #11876. Payouts are now optionally locked under a vesting schedule instead of + being transferred immediately, aligning long-term validator skin-in-the-game with the + protocol's bonding window. + + ## Changes to `pallet-vesting` + + - New `VestedPayout` trait method `add_to_vesting(source, dest, amount, duration, start_at)`. + Transfers `amount` from `source` into a new or merged vesting schedule on `dest` starting + at `start_at` and lasting `duration` blocks. + - If a schedule with the same `starting_block` already exists it is merged rather than + occupying an additional slot, preventing slot exhaustion within a bonding window. + - Two new `WeightInfo` methods: `add_to_vesting_create` and `add_to_vesting_merge`. + - **FRAME instance support** (SRLabs finding #674): `pallet-vesting` now supports multiple + instances via the standard `I: 'static = ()` generic parameter. This is a **breaking + change**: every `impl pallet_vesting::Config` must now add `type LockId: Get` + to select the currency lock key for that instance. Use + `pallet_vesting::DEFAULT_VESTING_LOCK_ID` (`*b"vesting "`) for the default instance. + The validator incentive vesting uses `Instance1` with lock id `*b"stkinctv"`, keeping + user vesting schedules and validator incentive schedules in fully separate storage + namespaces and under separate currency locks. + + ## Changes to `pallet-staking-async` + + ### New adapter trait + + `ValidatorIncentivePayout` decouples payout delivery + from staking-async so runtimes without pallet-vesting can compile. + + Two provided implementations: + - `LiquidIncentivePayout` — immediate transfer via `fungible::Mutate`. + - `VestedIncentivePayout` — calls `V::add_to_vesting`; falls back to liquid when + `duration` is zero (disabled) or pallet-vesting returns an error. + + ### New `Config` items (all `#[pallet::no_default]`) + + | Item | Purpose | + |------|---------| + | `VestingDuration` | Length of each vesting window in blocks. Set to `0` to disable vesting. | + | `VestingBlockNumberProvider` | Block-number provider whose space matches pallet-vesting's clock (relay-chain blocks on Asset Hub). | + | `ValidatorIncentivePayout` | Selects the payout adapter (`LiquidIncentivePayout` or `VestedIncentivePayout`). | + + ### New storage item + + `VestingEpochStart` (`StorageValue<_, BlockNumber, OptionQuery>`) — snapshotted at each + `BondingDuration`-era boundary. Used as the `starting_block` merge key so all incentive + payouts within one window accumulate into a single vesting slot. + + ### New event + + `ValidatorIncentiveForcedLiquid { era, validator_stash, amount }` — emitted when the + vested path fails and the liquid fallback succeeds. Allows operators to detect vesting-slot + exhaustion at runtime. + + ### Runtime wiring + + Both reference runtimes are wired up: + - `staking-async/runtimes/parachain`: `VestedIncentivePayout` with a one-year vesting + window (`365 * DAYS` relay-chain blocks). + - `asset-hub-westend`: `VestedIncentivePayout` with `365 * RC_DAYS` relay-chain blocks via + `RelaychainDataProvider`. + + ## Migration + + No storage migration required. `VestingEpochStart` starts as `None`; incentive payouts + are delivered as liquid transfers until the first bonding-duration boundary is crossed. + + ## Upgrading existing `pallet-vesting` configs + + One new associated type must be added to every `impl pallet_vesting::Config`: + + ```rust + parameter_types! { + pub const VestingLockId: LockIdentifier = pallet_vesting::DEFAULT_VESTING_LOCK_ID; + } + impl pallet_vesting::Config for Runtime { + // ... existing items ... + type LockId = VestingLockId; + } + ``` + + ## Upgrading existing `pallet-staking-async` configs + + Three new associated types must be added to every `impl pallet_staking_async::Config`: + + ```rust + // Disable vesting (liquid transfers only): + type VestingDuration = ConstU64<0>; // or ConstU32<0> depending on BlockNumber + type VestingBlockNumberProvider = frame_system::Pallet; + type ValidatorIncentivePayout = pallet_staking_async::LiquidIncentivePayout; + + // Enable vesting (using a dedicated Instance1 to prevent lock collision): + type VestingDuration = ValidatorIncentiveVestingDuration; // e.g. 365 * DAYS + type VestingBlockNumberProvider = RelaychainDataProvider; // match pallet-vesting's clock + type ValidatorIncentivePayout = pallet_staking_async::VestedIncentivePayout< + Balances, + pallet_vesting::Pallet, + >; + ``` + +crates: +- name: pallet-staking-async + bump: major +- name: pallet-vesting + bump: major +- name: frame-support + bump: minor +- name: pallet-staking-async-parachain-runtime + bump: minor +- name: asset-hub-westend-runtime + bump: minor diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 0a15b1003f6f6..4a10266b6617b 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1850,6 +1850,7 @@ parameter_types! { pub const MinVestedTransfer: Balance = 100 * DOLLARS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: LockIdentifier = pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -1860,6 +1861,7 @@ impl pallet_vesting::Config for Runtime { type WeightInfo = pallet_vesting::weights::SubstrateWeight; type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; // `VestingInfo` encode length is 36bytes. 28 schedules gets encoded as 1009 bytes, which is the // highest number of schedules that encodes less than 2^10. const MAX_VESTING_SCHEDULES: u32 = 28; diff --git a/substrate/frame/nomination-pools/test-delegate-stake/src/mock.rs b/substrate/frame/nomination-pools/test-delegate-stake/src/mock.rs index abe697f387659..07cb83cf3a535 100644 --- a/substrate/frame/nomination-pools/test-delegate-stake/src/mock.rs +++ b/substrate/frame/nomination-pools/test-delegate-stake/src/mock.rs @@ -135,6 +135,9 @@ impl pallet_staking_async::Config for Runtime { type TargetList = pallet_staking_async::UseValidatorsMap; type EventListeners = (Pools, DelegatedStaking); type RcClientInterface = MockRcClient; + type VestingDuration = ConstU64<0>; + type VestingBlockNumberProvider = frame_system::Pallet; + type ValidatorIncentivePayout = pallet_staking_async::LiquidIncentivePayout; } parameter_types! { diff --git a/substrate/frame/staking-async/integration-tests/src/ah/mock.rs b/substrate/frame/staking-async/integration-tests/src/ah/mock.rs index 6984f764fa077..2bd1327abf344 100644 --- a/substrate/frame/staking-async/integration-tests/src/ah/mock.rs +++ b/substrate/frame/staking-async/integration-tests/src/ah/mock.rs @@ -491,6 +491,9 @@ impl pallet_staking_async::Config for Runtime { type RcClientInterface = RcClient; type WeightInfo = super::weights::StakingAsyncWeightInfo; + type VestingDuration = ConstU64<0>; + type VestingBlockNumberProvider = frame_system::Pallet; + type ValidatorIncentivePayout = pallet_staking_async::LiquidIncentivePayout; } // Session keys type that must match RC's SessionKeys. diff --git a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs index d9532684f2d0c..f3556bc75b95f 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/lib.rs @@ -193,6 +193,7 @@ impl frame_system::Config for Runtime { type MaxConsumers = frame_support::traits::ConstU32<16>; type MultiBlockMigrator = MultiBlockMigrations; type SingleBlockMigrations = Migrations; + type BaseCallFilter = ValidatorVestingCallFilter; } impl cumulus_pallet_weight_reclaim::Config for Runtime { @@ -483,6 +484,8 @@ parameter_types! { pub const MinVestedTransfer: Balance = 100 * CENTS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -494,6 +497,33 @@ impl pallet_vesting::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = weights::pallet_vesting::WeightInfo; type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; + type LockId = VestingLockId; +} + +parameter_types! { + pub const ValidatorVestingLockId: frame_support::traits::LockIdentifier = *b"stkinctv"; +} + +/// Blocks direct user access to `vested_transfer` on the validator-incentive vesting instance. We +/// don't include `force_vested_transfer` because it requires root origin, which bypasses all +/// filters. +pub struct ValidatorVestingCallFilter; +impl frame_support::traits::Contains for ValidatorVestingCallFilter { + fn contains(call: &RuntimeCall) -> bool { + !matches!(call, RuntimeCall::ValidatorVesting(pallet_vesting::Call::vested_transfer { .. })) + } +} + +impl pallet_vesting::Config for Runtime { + const MAX_VESTING_SCHEDULES: u32 = 100; + type BlockNumberProvider = RelayChainBlockNumberProvider; + type BlockNumberToBalance = ConvertInto; + type Currency = Balances; + type MinVestedTransfer = MinVestedTransfer; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = weights::pallet_vesting::WeightInfo; + type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; + type LockId = ValidatorVestingLockId; } parameter_types! { @@ -1207,6 +1237,7 @@ construct_runtime!( // Balances. Vesting: pallet_vesting = 100, + ValidatorVesting: pallet_vesting:: = 101, // AHN specific. Sudo: pallet_sudo = 110, @@ -1370,6 +1401,7 @@ mod benches { [cumulus_pallet_xcmp_queue, XcmpQueue] [pallet_treasury, Treasury] [pallet_vesting, Vesting] + [pallet_vesting, ValidatorVesting] [pallet_whitelist, Whitelist] [pallet_xcm_bridge_hub_router, ToRococo] [pallet_asset_conversion_ops, AssetConversionMigration] diff --git a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs index 89f1af3892d40..cd35d51ef112d 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/staking.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/staking.rs @@ -425,6 +425,8 @@ parameter_types! { pub const MaxNominations: u32 = ::LIMIT as u32; pub const MaxEraDuration: u64 = RelaySessionDuration::get() as u64 * RELAY_CHAIN_SLOT_DURATION_MILLIS as u64 * SessionsPerEra::get() as u64; pub MaxPruningItems: u32 = 100; + /// Incentives vest over one year in relay-chain blocks (~5.26M at 6s/block). + pub const ValidatorIncentiveVestingDuration: BlockNumber = 365 * DAYS; } impl pallet_staking_async::Config for Runtime { @@ -464,6 +466,12 @@ impl pallet_staking_async::Config for Runtime { type PlanningEraOffset = pallet_staking_async::PlanningEraOffsetOf>; type RcClientInterface = StakingRcClient; + type VestingDuration = ValidatorIncentiveVestingDuration; + type VestingBlockNumberProvider = RelayChainBlockNumberProvider; + type ValidatorIncentivePayout = pallet_staking_async::VestedIncentivePayout< + Balances, + pallet_vesting::Pallet, + >; } // Relay chain session keys matching Westend configuration. @@ -762,7 +770,10 @@ where mod tests { use super::*; use frame_election_provider_support::ElectionProvider; - use frame_support::weights::constants::{WEIGHT_PROOF_SIZE_PER_KB, WEIGHT_REF_TIME_PER_MILLIS}; + use frame_support::{ + traits::Contains, + weights::constants::{WEIGHT_PROOF_SIZE_PER_KB, WEIGHT_REF_TIME_PER_MILLIS}, + }; use pallet_election_provider_multi_block::{ self as mb, signed::WeightInfo as _, unsigned::WeightInfo as _, }; @@ -784,6 +795,89 @@ mod tests { ); } + #[test] + fn validator_vesting_call_filter_blocks_vested_transfer() { + // Permisionless `vested_transfer` is not allowed on the ValidatorVesting instance. + let call = RuntimeCall::ValidatorVesting(pallet_vesting::Call::vested_transfer { + target: AccountId::from([42u8; 32]).into(), + schedule: pallet_vesting::VestingInfo::new(MinVestedTransfer::get(), 1, 0), + }); + assert!(!ValidatorVestingCallFilter::contains(&call)); + } + + #[test] + fn validator_vesting_call_filter_allows_force_vested_transfer() { + // Since `force_vested_transfer` is root-only, it should always be accessible since + // it bypasses all filters. + let call = RuntimeCall::ValidatorVesting(pallet_vesting::Call::force_vested_transfer { + source: AccountId::from([1u8; 32]).into(), + target: AccountId::from([2u8; 32]).into(), + schedule: pallet_vesting::VestingInfo::new(MinVestedTransfer::get(), 1, 0), + }); + assert!(ValidatorVestingCallFilter::contains(&call)); + } + + #[test] + fn validator_vesting_call_filter_allows_user_facing_calls() { + // We must keep `vest` and `vest_other` open so holders can unlock their funds. + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::ValidatorVesting( + pallet_vesting::Call::vest {} + ))); + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::ValidatorVesting( + pallet_vesting::Call::vest_other { target: AccountId::from([1u8; 32]).into() } + ))); + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::ValidatorVesting( + pallet_vesting::Call::merge_schedules { schedule1_index: 0, schedule2_index: 1 } + ))); + // An unrelated pallet must also pass through. + assert!(ValidatorVestingCallFilter::contains(&RuntimeCall::Timestamp( + pallet_timestamp::Call::set { now: 0 } + ))); + } + + #[test] + fn add_to_vesting_works_bypassing_call_filter() { + // Since `add_to_vesting` is a plain internal Rust call (and not a dispatchable) it is + // always allowed as it does not go through filtering. + use frame_support::traits::tokens::VestedPayout; + sp_io::TestExternalities::default().execute_with(|| { + let source = AccountId::from([1u8; 32]); + let dest = AccountId::from([2u8; 32]); + let amount = MinVestedTransfer::get(); + + frame_support::assert_ok!(Balances::force_set_balance( + RuntimeOrigin::root(), + source.clone().into(), + amount + ExistentialDeposit::get(), + )); + + frame_support::assert_ok!( + as VestedPayout< + AccountId, + Balance, + >>::add_to_vesting(&source, &dest, amount, 20u32, 1u32,) + ); + + assert!( + pallet_vesting::Vesting::::get(&dest).is_some() + ); + }); + } + + #[test] + fn validator_vesting_call_filter_is_the_base_call_filter() { + // Verify that the runtime's BaseCallFilter is our filter, not `Everything`. This + // ensures the dispatchable-blocking is actually wired into the extrinsic pipeline. + let blocked = RuntimeCall::ValidatorVesting(pallet_vesting::Call::vested_transfer { + target: AccountId::from([0u8; 32]).into(), + schedule: pallet_vesting::VestingInfo::new(MinVestedTransfer::get(), 1, 0), + }); + assert!( + !::BaseCallFilter::contains(&blocked), + "BaseCallFilter must block ValidatorVesting::vested_transfer" + ); + } + #[test] fn fake_dot_preset_snapshot_capacity_covers_max_electing_voters() { sp_io::TestExternalities::default().execute_with(|| { diff --git a/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_vesting.rs b/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_vesting.rs index f461ad9cbc0a1..6a8aa8c9e9a74 100644 --- a/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_vesting.rs +++ b/substrate/frame/staking-async/runtimes/parachain/src/weights/pallet_vesting.rs @@ -262,4 +262,12 @@ impl pallet_vesting::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_create(_l: u32, _s: u32) -> Weight { + Weight::zero() + } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_merge(_l: u32, _s: u32) -> Weight { + Weight::zero() + } } diff --git a/substrate/frame/staking-async/runtimes/rc/src/lib.rs b/substrate/frame/staking-async/runtimes/rc/src/lib.rs index f2e37f4d26447..420e01653694b 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/lib.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/lib.rs @@ -1266,6 +1266,8 @@ parameter_types! { pub const MinVestedTransfer: Balance = 100 * CENTS; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Runtime { @@ -1276,6 +1278,7 @@ impl pallet_vesting::Config for Runtime { type WeightInfo = weights::pallet_vesting::WeightInfo; type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; const MAX_VESTING_SCHEDULES: u32 = 28; } diff --git a/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_vesting.rs b/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_vesting.rs index 25d1c9eab7146..855db93382651 100644 --- a/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_vesting.rs +++ b/substrate/frame/staking-async/runtimes/rc/src/weights/pallet_vesting.rs @@ -259,4 +259,12 @@ impl pallet_vesting::WeightInfo for WeightInfo { .saturating_add(T::DbWeight::get().reads(4)) .saturating_add(T::DbWeight::get().writes(3)) } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_create(_l: u32, _s: u32) -> Weight { + Weight::zero() + } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_merge(_l: u32, _s: u32) -> Weight { + Weight::zero() + } } diff --git a/substrate/frame/staking-async/src/lib.rs b/substrate/frame/staking-async/src/lib.rs index 79c57b29b624c..0936f51a7e422 100644 --- a/substrate/frame/staking-async/src/lib.rs +++ b/substrate/frame/staking-async/src/lib.rs @@ -224,7 +224,10 @@ use codec::{Decode, DecodeWithMemTracking, Encode, HasCompact, MaxEncodedLen}; use frame_election_provider_support::ElectionProvider; use frame_support::{ traits::{ - tokens::fungible::{Credit, Debt}, + tokens::{ + fungible::{Credit, Debt, Mutate as FunMutate}, + Preservation, VestedPayout, + }, ConstU32, Contains, Get, LockIdentifier, }, BoundedVec, DebugNoBound, DefaultNoBound, EqNoBound, PartialEqNoBound, WeakBoundedVec, @@ -233,7 +236,7 @@ use frame_system::pallet_prelude::BlockNumberFor; use ledger::LedgerIntegrityState; use scale_info::TypeInfo; use sp_runtime::{ - traits::{AtLeast32BitUnsigned, One, StaticLookup, UniqueSaturatedInto}, + traits::{AtLeast32BitUnsigned, One, StaticLookup, UniqueSaturatedInto, Zero}, BoundedBTreeMap, Debug, Perbill, Saturating, }; use sp_staking::{EraIndex, ExposurePage, PagedExposureMetadata, SessionIndex}; @@ -704,6 +707,70 @@ where } } +/// Adapter trait for delivering validator self-stake incentive payouts. +pub trait ValidatorIncentivePayout { + /// Transfer `amount` from `source` to `dest` with a vesting schedule defined by `start_at` and + /// `duration`. Implementations that do not vest may ignore both parameters. + fn pay( + source: &AccountId, + dest: &AccountId, + amount: Balance, + start_at: BlockNumber, + duration: BlockNumber, + ) -> sp_runtime::DispatchResult; +} + +/// Liquid payout adapter — funds arrive immediately with no vesting lock. +pub struct LiquidIncentivePayout(core::marker::PhantomData); + +impl ValidatorIncentivePayout + for LiquidIncentivePayout +where + C: FunMutate, + AccountId: Eq, + Balance: Copy, + BlockNumber: Copy, +{ + fn pay( + source: &AccountId, + dest: &AccountId, + amount: Balance, + _start_at: BlockNumber, + _duration: BlockNumber, + ) -> sp_runtime::DispatchResult { + C::transfer(source, dest, amount, Preservation::Expendable).map(|_| ()) + } +} + +/// Vested payout adapter — funds arrive under a linear vesting schedule. +/// Falls back to a liquid transfer when `duration` is zero. +pub struct VestedIncentivePayout(core::marker::PhantomData<(C, V)>); + +impl + ValidatorIncentivePayout for VestedIncentivePayout +where + C: FunMutate, + V: VestedPayout, + AccountId: Eq, + Balance: Copy, + BlockNumber: Zero + Copy, +{ + fn pay( + source: &AccountId, + dest: &AccountId, + amount: Balance, + start_at: BlockNumber, + duration: BlockNumber, + ) -> sp_runtime::DispatchResult { + if duration.is_zero() { + // This happens for example when `VestingEpochStart` has not yet been set. + C::transfer(source, dest, amount, Preservation::Expendable).map(|_| ()) + } else { + V::add_to_vesting(source, dest, amount, duration, start_at) + } + } +} + /// A smart type to determine the [`Config::PlanningEraOffset`], given: /// /// * Expected relay session duration, `RS` diff --git a/substrate/frame/staking-async/src/mock.rs b/substrate/frame/staking-async/src/mock.rs index 907f25c63f337..53d6a311c03b3 100644 --- a/substrate/frame/staking-async/src/mock.rs +++ b/substrate/frame/staking-async/src/mock.rs @@ -560,6 +560,9 @@ impl Config for Test { type CurrencyToVote = SaturatingCurrencyToVote; type Slash = Dap; type RuntimeHoldReason = RuntimeHoldReason; + type VestingDuration = ConstU64<0>; + type VestingBlockNumberProvider = frame_system::Pallet; + type ValidatorIncentivePayout = LiquidIncentivePayout; type WeightInfo = (); } diff --git a/substrate/frame/staking-async/src/pallet/impls.rs b/substrate/frame/staking-async/src/pallet/impls.rs index e97fed813f99f..ffe73efcb60ff 100644 --- a/substrate/frame/staking-async/src/pallet/impls.rs +++ b/substrate/frame/staking-async/src/pallet/impls.rs @@ -26,7 +26,7 @@ use crate::{ weights::WeightInfo, BalanceOf, Exposure, Forcing, LedgerIntegrityState, MaxNominationsOf, Nominations, NominationsQuota, PositiveImbalanceOf, PotAccountProvider, RewardDestination, RewardKind, - RewardPot, SnapshotStatus, StakingLedger, ValidatorPrefs, STAKING_ID, + RewardPot, SnapshotStatus, StakingLedger, ValidatorIncentivePayout, ValidatorPrefs, STAKING_ID, }; use alloc::{boxed::Box, vec, vec::Vec}; use frame_election_provider_support::{ @@ -725,7 +725,9 @@ impl Pallet { /// Transfer validator incentive from era pot to the validator's payout account. /// - /// This is a direct liquid transfer. Future PRs may introduce vesting via a trait. + /// Delegates delivery to [`Config::ValidatorIncentivePayout`]. On failure, falls back to a + /// direct liquid transfer and emits [`Event::ValidatorIncentiveForcedLiquid`] so operators + /// can detect vesting schedule slot exhaustion. fn transfer_validator_incentive(era: EraIndex, stash: &T::AccountId, amount: BalanceOf) { let Some(dest) = Self::payee(Stash(stash.clone())) else { Self::deposit_event(Event::::Unexpected(UnexpectedKind::MissingPayee { @@ -744,13 +746,22 @@ impl Pallet { crate::RewardKind::ValidatorSelfStake, )); - match T::Currency::transfer( + // Use the current vesting window's start block as the merge key. If this is not yet set + // (i.e. before the first epoch boundary), the duration is forced to zero which will cause + // the adapter to deliver liquid. + let (start_at, duration) = match VestingEpochStart::::get() { + Some(start) => (start, T::VestingDuration::get()), + None => (BlockNumberFor::::zero(), BlockNumberFor::::zero()), + }; + + match T::ValidatorIncentivePayout::pay( &incentive_pot, &payout_account, amount, - Preservation::Expendable, + start_at, + duration, ) { - Ok(_) => { + Ok(()) => { Self::deposit_event(Event::::ValidatorIncentivePaid { era, validator_stash: stash.clone(), @@ -759,11 +770,36 @@ impl Pallet { }); }, Err(e) => { - log!(warn, "Failed to transfer liquid incentive: {:?}", e); - Self::deposit_event(Event::::Unexpected( - UnexpectedKind::ValidatorIncentiveTransferFailed { era }, - )); - defensive!("Validator incentive liquid transfer failed"); + // Vested delivery failed (e.g. AtMaxVestingSchedules). We then attempt liquid + // fallback so that validators are never silently starved of their incentive. + log!(warn, "Incentive pay failed ({:?}), falling back to liquid", e); + Self::deposit_event(Event::::ValidatorIncentiveForcedLiquid { + era, + validator_stash: stash.clone(), + amount, + }); + match T::Currency::transfer( + &incentive_pot, + &payout_account, + amount, + Preservation::Expendable, + ) { + Ok(_) => { + Self::deposit_event(Event::::ValidatorIncentivePaid { + era, + validator_stash: stash.clone(), + dest, + amount, + }); + }, + Err(e2) => { + log!(warn, "Liquid fallback also failed: {:?}", e2); + Self::deposit_event(Event::::Unexpected( + UnexpectedKind::ValidatorIncentiveTransferFailed { era }, + )); + defensive!("Validator incentive transfer failed"); + }, + } }, } } diff --git a/substrate/frame/staking-async/src/pallet/mod.rs b/substrate/frame/staking-async/src/pallet/mod.rs index 98d81ee5feac6..8b1c56017193d 100644 --- a/substrate/frame/staking-async/src/pallet/mod.rs +++ b/substrate/frame/staking-async/src/pallet/mod.rs @@ -422,6 +422,35 @@ pub mod pallet { /// another way (such as pools). type Filter: Contains; + /// The number of blocks over which validator self-stake incentives vest. + /// + /// Set to `0` to deliver incentives as a liquid transfer (useful for test environments or + /// runtimes without `pallet-vesting`). + #[pallet::no_default] + type VestingDuration: Get>; + + /// Block number provider used to snapshot [`VestingEpochStart`]. + /// + /// Must use the **same** block-number space as `pallet_vesting`'s + /// `BlockNumberProvider` so that `start_at` keys round-trip correctly. + /// On parachains this should be `RelaychainDataProvider` when + /// pallet-vesting is also configured with relay-chain block numbers. + #[pallet::no_default] + type VestingBlockNumberProvider: sp_runtime::traits::BlockNumberProvider< + BlockNumber = BlockNumberFor, + >; + + /// Adapter that delivers validator self-stake incentive payouts. + /// + /// Wire [`crate::VestedIncentivePayout`] for vested delivery, or + /// [`crate::LiquidIncentivePayout`] for immediate liquid delivery. + #[pallet::no_default] + type ValidatorIncentivePayout: crate::ValidatorIncentivePayout< + Self::AccountId, + BalanceOf, + BlockNumberFor, + >; + /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; } @@ -579,6 +608,18 @@ pub mod pallet { OptionQuery, >; + /// The block number at which the current vesting window started. + /// + /// Snapshotted each time a new [`Config::BondingDuration`]-era window begins (i.e. + /// `era % BondingDuration == 0`). Used as the `starting_block` merge key in + /// [`crate::VestedIncentivePayout`] so all incentive payouts within the same window + /// accumulate into one vesting schedule slot rather than consuming a new slot per era. + /// + /// `None` until the first epoch boundary is crossed; `None` forces incentive payouts + /// to be delivered as a liquid transfer. + #[pallet::storage] + pub type VestingEpochStart = StorageValue<_, BlockNumberFor, OptionQuery>; + /// Whether nominators are slashable or not. /// /// - When set to `true` (default), nominators are slashed along with validators and must wait @@ -1408,6 +1449,13 @@ pub mod pallet { hard_cap_self_stake: BalanceOf, slope_factor: Perbill, }, + /// The vested incentive delivery failed (e.g. `AtMaxVestingSchedules`) and the pallet + /// fell back to a direct liquid transfer. + ValidatorIncentiveForcedLiquid { + era: EraIndex, + validator_stash: T::AccountId, + amount: BalanceOf, + }, } /// Represents unexpected or invariant-breaking conditions encountered during execution. diff --git a/substrate/frame/staking-async/src/session_rotation.rs b/substrate/frame/staking-async/src/session_rotation.rs index 443237cccde17..07dd397a908ca 100644 --- a/substrate/frame/staking-async/src/session_rotation.rs +++ b/substrate/frame/staking-async/src/session_rotation.rs @@ -85,7 +85,7 @@ use frame_support::{ weights::WeightMeter, }; use pallet_staking_async_rc_client::RcClientInterface; -use sp_runtime::{Perbill, Percent, Saturating}; +use sp_runtime::{traits::BlockNumberProvider, Perbill, Percent, Saturating}; use sp_staking::{ currency_to_vote::CurrencyToVote, Exposure, Page, PagedExposureMetadata, SessionIndex, StakerRewardCalculator, @@ -774,6 +774,13 @@ impl Rotator { Self::start_era_inc_active_era(new_era_start_timestamp); Self::start_era_update_bonded_eras(starting_era, starting_session); + // Snapshot the vesting epoch start block when a new bonding-duration window begins. + let bonding_duration = T::BondingDuration::get(); + if bonding_duration != 0 && starting_era % bonding_duration == 0 { + let now = T::VestingBlockNumberProvider::current_block_number(); + VestingEpochStart::::put(now); + } + // Snapshot the current nominators slashable setting for this era. // Cleanup will happen via lazy pruning at HistoryDepth. ErasNominatorsSlashable::::insert(starting_era, AreNominatorsSlashable::::get()); diff --git a/substrate/frame/staking-async/src/tests/validator_incentive.rs b/substrate/frame/staking-async/src/tests/validator_incentive.rs index d1ab82af5985c..3721dc94a637f 100644 --- a/substrate/frame/staking-async/src/tests/validator_incentive.rs +++ b/substrate/frame/staking-async/src/tests/validator_incentive.rs @@ -608,8 +608,107 @@ fn missing_payee_emits_unexpected_and_skips_payout() { // ===== Defensive path tests ===== +// ===== VestingEpochStart snapshot tests ===== + +#[test] +fn vesting_epoch_start_none_before_first_boundary() { + ExtBuilder::default().build_and_execute(|| { + // GIVEN: chain starts — no epoch boundary has been crossed yet. + // THEN: VestingEpochStart is not set. + assert!(VestingEpochStart::::get().is_none()); + + // Advance to era 1 and 2 (BondingDuration = 3, so boundary is at era 3). + Session::roll_until_active_era(2); + assert!(VestingEpochStart::::get().is_none()); + }); +} + +#[test] +fn vesting_epoch_start_snapshotted_at_bonding_duration_boundary() { + ExtBuilder::default().build_and_execute(|| { + // BondingDuration = 3 in the test mock, so VestingEpochStart is set when + // starting_era % 3 == 0, i.e. when era 3 (starting_era=3) begins. + let block_before = System::block_number(); + Session::roll_until_active_era(3); + + let snapshot = VestingEpochStart::::get(); + assert!(snapshot.is_some(), "VestingEpochStart should be set after first boundary"); + // The snapshot must be at or after the block we were at before advancing. + assert!(snapshot.unwrap() >= block_before); + }); +} + +#[test] +fn vesting_epoch_start_updated_on_next_boundary() { + ExtBuilder::default().build_and_execute(|| { + // Advance past the first boundary (era 3). + Session::roll_until_active_era(3); + let first_snapshot = VestingEpochStart::::get().unwrap(); + + // Advance past the second boundary (era 6 = 2 * BondingDuration). + Session::roll_until_active_era(6); + let second_snapshot = VestingEpochStart::::get().unwrap(); + + // The second snapshot should be strictly later than the first. + assert!( + second_snapshot > first_snapshot, + "second snapshot ({second_snapshot}) should be after first ({first_snapshot})" + ); + }); +} + +#[test] +fn vesting_epoch_start_unchanged_between_boundaries() { + ExtBuilder::default().build_and_execute(|| { + // Cross the first boundary. + Session::roll_until_active_era(3); + let snapshot_at_3 = VestingEpochStart::::get().unwrap(); + + // Era 4 and 5 should not change the snapshot. + Session::roll_until_active_era(4); + assert_eq!(VestingEpochStart::::get().unwrap(), snapshot_at_3); + Session::roll_until_active_era(5); + assert_eq!(VestingEpochStart::::get().unwrap(), snapshot_at_3); + }); +} + +#[test] +fn forced_liquid_fallback_event_emitted_on_vested_pay_failure() { + // Verify that ValidatorIncentiveForcedLiquid is emitted when the primary payout + // adapter fails and the liquid fallback succeeds. + // We simulate by draining the pot AFTER the era snapshot (so the era pot is empty + // for the primary path) — but keep enough balance elsewhere so the second transfer + // (direct liquid) also has no source. Instead, we verify the liquid path IS taken + // by observing that no ForcedLiquid event is emitted when the primary succeeds. + // + // In the current mock, ValidatorIncentivePayout = LiquidIncentivePayout, so the + // primary path succeeds and ForcedLiquid is never emitted. + ExtBuilder::default().build_and_execute(|| { + let alice = 11; + + setup_incentive_with_budget(45, 5); + Session::roll_until_active_era(2); + Eras::::reward_active_era(vec![(alice, 1)]); + Session::roll_until_active_era(3); + + make_all_reward_payment(2); + + // No forced-liquid event should have been emitted (liquid path succeeded on first try). + let events = staking_events_since_last_call(); + assert!( + !events.iter().any(|e| matches!(e, Event::ValidatorIncentiveForcedLiquid { .. })), + "ForcedLiquid should not be emitted when primary payout succeeds" + ); + // But ValidatorIncentivePaid should be there. + assert!( + events.iter().any(|e| matches!(e, Event::ValidatorIncentivePaid { .. })), + "ValidatorIncentivePaid should be emitted on success" + ); + }); +} + #[test] -#[should_panic(expected = "Validator incentive liquid transfer failed")] +#[should_panic(expected = "Validator incentive transfer failed")] fn defensive_panic_on_transfer_failure() { ExtBuilder::default().build_and_execute(|| { let alice = 11; // validator diff --git a/substrate/frame/support/src/traits/tokens/misc.rs b/substrate/frame/support/src/traits/tokens/misc.rs index b5f34496a3c69..fc6a09c146434 100644 --- a/substrate/frame/support/src/traits/tokens/misc.rs +++ b/substrate/frame/support/src/traits/tokens/misc.rs @@ -464,4 +464,22 @@ pub trait VestedPayout { duration: Self::BlockNumber, start_at: Option, ) -> sp_runtime::DispatchResult; + + /// Transfer `amount` from `source` to `dest`, merging with the existing vesting schedule + /// whose `starting_block` equals `start_at`, or creating a new schedule if none exists. + /// On the merge path `MinVestedTransfer` is not enforced, allowing sub-minimum era payouts + /// to accumulate into the epoch's schedule. + /// + /// # Warning — since `vested_transfer` is a permissionless extrinsic, an external actor can + /// fill all remaining schedule slots on `dest`. The next schedule creation (not merge) will + /// then fail with `AtMaxVestingSchedules`. Solutions to avoid this may be to simply prevent + /// permissionless `vested_transfer` or to fall back to a liquid transfer and emit an + /// observable event. + fn add_to_vesting( + source: &AccountId, + dest: &AccountId, + amount: Balance, + duration: Self::BlockNumber, + start_at: Self::BlockNumber, + ) -> sp_runtime::DispatchResult; } diff --git a/substrate/frame/vesting/precompiles/src/mock.rs b/substrate/frame/vesting/precompiles/src/mock.rs index dfcf9116e5766..93a36edddfee3 100644 --- a/substrate/frame/vesting/precompiles/src/mock.rs +++ b/substrate/frame/vesting/precompiles/src/mock.rs @@ -76,6 +76,8 @@ parameter_types! { pub const MinVestedTransfer: u64 = 256 * 2; pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); + pub const VestingLockId: frame_support::traits::LockIdentifier = + pallet_vesting::DEFAULT_VESTING_LOCK_ID; } impl pallet_vesting::Config for Test { @@ -87,6 +89,7 @@ impl pallet_vesting::Config for Test { type WeightInfo = (); type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; } #[derive_impl(pallet_revive::config_preludes::TestDefaultConfig)] diff --git a/substrate/frame/vesting/src/benchmarking.rs b/substrate/frame/vesting/src/benchmarking.rs index 3797ee9079db0..370e5172ba2c3 100644 --- a/substrate/frame/vesting/src/benchmarking.rs +++ b/substrate/frame/vesting/src/benchmarking.rs @@ -28,10 +28,10 @@ use crate::*; const SEED: u32 = 0; -type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; +type BalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; -fn add_locks(who: &T::AccountId, n: u8) { +fn add_locks, I: 'static>(who: &T::AccountId, n: u8) { for id in 0..n { let lock_id = [id; 8]; let locked = 256_u32; @@ -40,10 +40,10 @@ fn add_locks(who: &T::AccountId, n: u8) { } } -fn add_vesting_schedules( +fn add_vesting_schedules, I: 'static>( target: &T::AccountId, n: u32, -) -> Result, &'static str> { +) -> Result, &'static str> { let min_transfer = T::MinVestedTransfer::get(); let locked = min_transfer.checked_mul(&20_u32.into()).unwrap(); // Schedule has a duration of 20. @@ -51,43 +51,43 @@ fn add_vesting_schedules( let starting_block = 1_u32; let source = account("source", 0, SEED); - T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); T::BlockNumberProvider::set_block_number(BlockNumberFor::::zero()); - let mut total_locked: BalanceOf = Zero::zero(); + let mut total_locked: BalanceOf = Zero::zero(); for _ in 0..n { total_locked += locked; let schedule = VestingInfo::new(locked, per_block, starting_block.into()); - assert_ok!(Pallet::::do_vested_transfer(&source, target, schedule)); + assert_ok!(Pallet::::do_vested_transfer(&source, target, schedule)); // Top up to guarantee we can always transfer another schedule. - T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); } Ok(total_locked) } -#[benchmarks] +#[instance_benchmarks] mod benchmarks { use super::*; #[benchmark] fn vest_locked( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<1, T::MAX_VESTING_SCHEDULES>, ) -> Result<(), BenchmarkError> { let caller = whitelisted_caller(); T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); - add_locks::(&caller, l as u8); - let expected_balance = add_vesting_schedules::(&caller, s)?; + add_locks::(&caller, l as u8); + let expected_balance = add_vesting_schedules::(&caller, s)?; // At block zero, everything is vested. assert_eq!(frame_system::Pallet::::block_number(), BlockNumberFor::::zero()); assert_eq!( - Pallet::::vesting_balance(&caller), + Pallet::::vesting_balance(&caller), Some(expected_balance), "Vesting schedule not added", ); @@ -97,7 +97,7 @@ mod benchmarks { // Nothing happened since everything is still vested. assert_eq!( - Pallet::::vesting_balance(&caller), + Pallet::::vesting_balance(&caller), Some(expected_balance), "Vesting schedule was removed", ); @@ -107,20 +107,20 @@ mod benchmarks { #[benchmark] fn vest_unlocked( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<1, T::MAX_VESTING_SCHEDULES>, ) -> Result<(), BenchmarkError> { let caller = whitelisted_caller(); T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); - add_locks::(&caller, l as u8); - add_vesting_schedules::(&caller, s)?; + add_locks::(&caller, l as u8); + add_vesting_schedules::(&caller, s)?; // At block 21, everything is unlocked. T::BlockNumberProvider::set_block_number(21_u32.into()); assert_eq!( - Pallet::::vesting_balance(&caller), - Some(BalanceOf::::zero()), + Pallet::::vesting_balance(&caller), + Some(BalanceOf::::zero()), "Vesting schedule still active", ); @@ -128,27 +128,31 @@ mod benchmarks { vest(RawOrigin::Signed(caller.clone())); // Vesting schedule is removed! - assert_eq!(Pallet::::vesting_balance(&caller), None, "Vesting schedule was not removed",); + assert_eq!( + Pallet::::vesting_balance(&caller), + None, + "Vesting schedule was not removed", + ); Ok(()) } #[benchmark] fn vest_other_locked( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<1, T::MAX_VESTING_SCHEDULES>, ) -> Result<(), BenchmarkError> { let other = account::("other", 0, SEED); let other_lookup = T::Lookup::unlookup(other.clone()); T::Currency::make_free_balance_be(&other, T::Currency::minimum_balance()); - add_locks::(&other, l as u8); - let expected_balance = add_vesting_schedules::(&other, s)?; + add_locks::(&other, l as u8); + let expected_balance = add_vesting_schedules::(&other, s)?; // At block zero, everything is vested. assert_eq!(frame_system::Pallet::::block_number(), BlockNumberFor::::zero()); assert_eq!( - Pallet::::vesting_balance(&other), + Pallet::::vesting_balance(&other), Some(expected_balance), "Vesting schedule not added", ); @@ -160,7 +164,7 @@ mod benchmarks { // Nothing happened since everything is still vested. assert_eq!( - Pallet::::vesting_balance(&other), + Pallet::::vesting_balance(&other), Some(expected_balance), "Vesting schedule was removed", ); @@ -170,21 +174,21 @@ mod benchmarks { #[benchmark] fn vest_other_unlocked( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<1, { T::MAX_VESTING_SCHEDULES }>, ) -> Result<(), BenchmarkError> { let other = account::("other", 0, SEED); let other_lookup = T::Lookup::unlookup(other.clone()); T::Currency::make_free_balance_be(&other, T::Currency::minimum_balance()); - add_locks::(&other, l as u8); - add_vesting_schedules::(&other, s)?; + add_locks::(&other, l as u8); + add_vesting_schedules::(&other, s)?; // At block 21 everything is unlocked. T::BlockNumberProvider::set_block_number(21_u32.into()); assert_eq!( - Pallet::::vesting_balance(&other), - Some(BalanceOf::::zero()), + Pallet::::vesting_balance(&other), + Some(BalanceOf::::zero()), "Vesting schedule still active", ); @@ -194,27 +198,31 @@ mod benchmarks { vest_other(RawOrigin::Signed(caller.clone()), other_lookup); // Vesting schedule is removed. - assert_eq!(Pallet::::vesting_balance(&other), None, "Vesting schedule was not removed",); + assert_eq!( + Pallet::::vesting_balance(&other), + None, + "Vesting schedule was not removed", + ); Ok(()) } #[benchmark] fn vested_transfer( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<0, { T::MAX_VESTING_SCHEDULES - 1 }>, ) -> Result<(), BenchmarkError> { let caller = whitelisted_caller(); - T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); + T::Currency::make_free_balance_be(&caller, BalanceOf::::max_value()); let target = account::("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); // Give target existing locks. T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); - add_locks::(&target, l as u8); + add_locks::(&target, l as u8); // Add one vesting schedules. let orig_balance = T::Currency::free_balance(&target); - let mut expected_balance = add_vesting_schedules::(&target, s)?; + let mut expected_balance = add_vesting_schedules::(&target, s)?; let transfer_amount = T::MinVestedTransfer::get(); let per_block = transfer_amount.checked_div(&20_u32.into()).unwrap(); @@ -231,7 +239,7 @@ mod benchmarks { "Transfer didn't happen", ); assert_eq!( - Pallet::::vesting_balance(&target), + Pallet::::vesting_balance(&target), Some(expected_balance), "Lock not correctly updated", ); @@ -241,21 +249,21 @@ mod benchmarks { #[benchmark] fn force_vested_transfer( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<0, { T::MAX_VESTING_SCHEDULES - 1 }>, ) -> Result<(), BenchmarkError> { let source = account::("source", 0, SEED); let source_lookup = T::Lookup::unlookup(source.clone()); - T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); let target = account::("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); // Give target existing locks. T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); - add_locks::(&target, l as u8); + add_locks::(&target, l as u8); // Add one less than max vesting schedules. let orig_balance = T::Currency::free_balance(&target); - let mut expected_balance = add_vesting_schedules::(&target, s)?; + let mut expected_balance = add_vesting_schedules::(&target, s)?; let transfer_amount = T::MinVestedTransfer::get(); let per_block = transfer_amount.checked_div(&20_u32.into()).unwrap(); @@ -272,7 +280,7 @@ mod benchmarks { "Transfer didn't happen", ); assert_eq!( - Pallet::::vesting_balance(&target), + Pallet::::vesting_balance(&target), Some(expected_balance), "Lock not correctly updated", ); @@ -282,25 +290,25 @@ mod benchmarks { #[benchmark] fn not_unlocking_merge_schedules( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<2, { T::MAX_VESTING_SCHEDULES }>, ) -> Result<(), BenchmarkError> { let caller = whitelisted_caller::(); // Give target existing locks. T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); - add_locks::(&caller, l as u8); + add_locks::(&caller, l as u8); // Add max vesting schedules. - let expected_balance = add_vesting_schedules::(&caller, s)?; + let expected_balance = add_vesting_schedules::(&caller, s)?; // Schedules are not vesting at block 0. assert_eq!(frame_system::Pallet::::block_number(), BlockNumberFor::::zero()); assert_eq!( - Pallet::::vesting_balance(&caller), + Pallet::::vesting_balance(&caller), Some(expected_balance), "Vesting balance should equal sum locked of all schedules", ); assert_eq!( - Vesting::::get(&caller).unwrap().len(), + Vesting::::get(&caller).unwrap().len(), s as usize, "There should be exactly max vesting schedules" ); @@ -314,14 +322,14 @@ mod benchmarks { 1_u32.into(), ); let expected_index = (s - 2) as usize; - assert_eq!(Vesting::::get(&caller).unwrap()[expected_index], expected_schedule); + assert_eq!(Vesting::::get(&caller).unwrap()[expected_index], expected_schedule); assert_eq!( - Pallet::::vesting_balance(&caller), + Pallet::::vesting_balance(&caller), Some(expected_balance), "Vesting balance should equal total locked of all schedules", ); assert_eq!( - Vesting::::get(&caller).unwrap().len(), + Vesting::::get(&caller).unwrap().len(), (s - 1) as usize, "Schedule count should reduce by 1" ); @@ -331,7 +339,7 @@ mod benchmarks { #[benchmark] fn unlocking_merge_schedules( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<2, { T::MAX_VESTING_SCHEDULES }>, ) -> Result<(), BenchmarkError> { // Destination used just for currency transfers in asserts. @@ -340,9 +348,9 @@ mod benchmarks { let caller = whitelisted_caller::(); // Give target existing locks. T::Currency::make_free_balance_be(&caller, T::Currency::minimum_balance()); - add_locks::(&caller, l as u8); + add_locks::(&caller, l as u8); // Add max vesting schedules. - let total_transferred = add_vesting_schedules::(&caller, s)?; + let total_transferred = add_vesting_schedules::(&caller, s)?; // Go to about half way through all the schedules duration. (They all start at 1, and have a // duration of 20 or 21). @@ -351,12 +359,12 @@ mod benchmarks { // block). let expected_balance = total_transferred / 2_u32.into(); assert_eq!( - Pallet::::vesting_balance(&caller), + Pallet::::vesting_balance(&caller), Some(expected_balance), "Vesting balance should reflect that we are half way through all schedules duration", ); assert_eq!( - Vesting::::get(&caller).unwrap().len(), + Vesting::::get(&caller).unwrap().len(), s as usize, "There should be exactly max vesting schedules" ); @@ -379,17 +387,17 @@ mod benchmarks { ); let expected_index = (s - 2) as usize; assert_eq!( - Vesting::::get(&caller).unwrap()[expected_index], + Vesting::::get(&caller).unwrap()[expected_index], expected_schedule, "New schedule is properly created and placed" ); assert_eq!( - Pallet::::vesting_balance(&caller), + Pallet::::vesting_balance(&caller), Some(expected_balance), "Vesting balance should equal half total locked of all schedules", ); assert_eq!( - Vesting::::get(&caller).unwrap().len(), + Vesting::::get(&caller).unwrap().len(), (s - 1) as usize, "Schedule count should reduce by 1" ); @@ -406,19 +414,19 @@ mod benchmarks { #[benchmark] fn force_remove_vesting_schedule( - l: Linear<0, { MaxLocksOf::::get() - 1 }>, + l: Linear<0, { MaxLocksOf::::get() - 1 }>, s: Linear<2, { T::MAX_VESTING_SCHEDULES }>, ) -> Result<(), BenchmarkError> { let source = account::("source", 0, SEED); - T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); let target = account::("target", 0, SEED); let target_lookup = T::Lookup::unlookup(target.clone()); T::Currency::make_free_balance_be(&target, T::Currency::minimum_balance()); // Give target existing locks. - add_locks::(&target, l as u8); - add_vesting_schedules::(&target, s)?; + add_locks::(&target, l as u8); + add_vesting_schedules::(&target, s)?; // The last vesting schedule. let schedule_index = s - 1; @@ -427,7 +435,7 @@ mod benchmarks { _(RawOrigin::Root, target_lookup, schedule_index); assert_eq!( - Vesting::::get(&target).unwrap().len(), + Vesting::::get(&target).unwrap().len(), schedule_index as usize, "Schedule count should reduce by 1" ); @@ -435,6 +443,95 @@ mod benchmarks { Ok(()) } + #[benchmark] + fn add_to_vesting_create( + l: Linear<0, { MaxLocksOf::::get() - 1 }>, + s: Linear<0, { T::MAX_VESTING_SCHEDULES - 1 }>, + ) -> Result<(), BenchmarkError> { + let source = account::("source", 0, SEED); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + + let dest = account::("dest", 0, SEED); + T::Currency::make_free_balance_be(&dest, T::Currency::minimum_balance()); + add_locks::(&dest, l as u8); + add_vesting_schedules::(&dest, s)?; + + T::BlockNumberProvider::set_block_number(BlockNumberFor::::zero()); + + // Use a "start_at" that won't match any existing schedules, which all use "starting_block" + // set to 1. + let start_at: BlockNumberFor = 100_u32.into(); + let duration: BlockNumberFor = 20_u32.into(); + let amount = T::MinVestedTransfer::get(); + + #[block] + { + as frame_support::traits::tokens::VestedPayout< + T::AccountId, + BalanceOf, + >>::add_to_vesting(&source, &dest, amount, duration, start_at) + .unwrap(); + } + + assert_eq!( + Vesting::::get(&dest).unwrap().len(), + s as usize + 1, + "Schedule should have been created" + ); + + Ok(()) + } + + #[benchmark] + fn add_to_vesting_merge( + l: Linear<0, { MaxLocksOf::::get() - 1 }>, + s: Linear<1, { T::MAX_VESTING_SCHEDULES }>, + ) -> Result<(), BenchmarkError> { + let source = account::("source", 0, SEED); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + + let dest = account::("dest", 0, SEED); + T::Currency::make_free_balance_be(&dest, T::Currency::minimum_balance()); + add_locks::(&dest, l as u8); + + // Add "s - 1" filler schedules (with "starting_block" of 1) and one target schedule (with + // "starting_block" of 2). + add_vesting_schedules::(&dest, s - 1)?; + + let amount = T::MinVestedTransfer::get(); + let start_at: BlockNumberFor = 2_u32.into(); + let duration: BlockNumberFor = 20_u32.into(); + T::BlockNumberProvider::set_block_number(BlockNumberFor::::zero()); + + // Create the schedule that will be merged into. + as frame_support::traits::tokens::VestedPayout< + T::AccountId, + BalanceOf, + >>::add_to_vesting(&source, &dest, amount, duration, start_at) + .unwrap(); + T::Currency::make_free_balance_be(&source, BalanceOf::::max_value()); + + assert_eq!(Vesting::::get(&dest).unwrap().len(), s as usize); + + #[block] + { + as frame_support::traits::tokens::VestedPayout< + T::AccountId, + BalanceOf, + >>::add_to_vesting(&source, &dest, amount, duration, start_at) + .unwrap(); + } + + // Ensure the schedule count is unchanged (the merge did not use a new slot). + assert_eq!( + Vesting::::get(&dest).unwrap().len(), + s as usize, + "Merge should not increase schedule count" + ); + + Ok(()) + } + impl_benchmark_test_suite! { Pallet, mock::ExtBuilder::default().existential_deposit(256).build(), diff --git a/substrate/frame/vesting/src/lib.rs b/substrate/frame/vesting/src/lib.rs index 06346cd176559..3aba97cf235ec 100644 --- a/substrate/frame/vesting/src/lib.rs +++ b/substrate/frame/vesting/src/lib.rs @@ -85,13 +85,20 @@ pub use pallet::*; pub use vesting_info::*; pub use weights::WeightInfo; -type BalanceOf = - <::Currency as Currency<::AccountId>>::Balance; -type MaxLocksOf = - <::Currency as LockableCurrency<::AccountId>>::MaxLocks; +/// Convenience alias for the balance type of a given `Config`. +pub type BalanceOf = + <>::Currency as Currency<::AccountId>>::Balance; +type MaxLocksOf = <>::Currency as LockableCurrency< + ::AccountId, +>>::MaxLocks; type AccountIdLookupOf = <::Lookup as StaticLookup>::Source; -const VESTING_ID: LockIdentifier = *b"vesting "; +/// The default `LockIdentifier` used by the default vesting pallet instance. +/// +/// Exposed as a public constant so runtime configs can reference it via +/// `parameter_types! { pub const VestingLockId: LockIdentifier = +/// pallet_vesting::DEFAULT_VESTING_LOCK_ID; }`. +pub const DEFAULT_VESTING_LOCK_ID: LockIdentifier = *b"vesting "; // A value placed in storage that represents the current version of the Vesting storage. // This value is used by `on_runtime_upgrade` to determine whether we run storage migration logic. @@ -129,10 +136,10 @@ impl VestingAction { } /// Pick the schedules that this action dictates should continue vesting undisturbed. - fn pick_schedules( + fn pick_schedules, I: 'static>( &self, - schedules: Vec, BlockNumberFor>>, - ) -> impl Iterator, BlockNumberFor>> + '_ { + schedules: Vec, BlockNumberFor>>, + ) -> impl Iterator, BlockNumberFor>> + '_ { schedules.into_iter().enumerate().filter_map(move |(index, schedule)| { if self.should_remove(index) { None @@ -144,8 +151,8 @@ impl VestingAction { } // Wrapper for `T::MAX_VESTING_SCHEDULES` to satisfy `trait Get`. -pub struct MaxVestingSchedulesGet(PhantomData); -impl Get for MaxVestingSchedulesGet { +pub struct MaxVestingSchedulesGet(PhantomData<(T, I)>); +impl, I: 'static> Get for MaxVestingSchedulesGet { fn get() -> u32 { T::MAX_VESTING_SCHEDULES } @@ -158,20 +165,21 @@ pub mod pallet { use frame_system::pallet_prelude::*; #[pallet::config] - pub trait Config: frame_system::Config { + pub trait Config: frame_system::Config { /// The overarching event type. #[allow(deprecated)] - type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; /// The currency trait. type Currency: LockableCurrency; /// Convert the block number into a balance. - type BlockNumberToBalance: Convert, BalanceOf>; + type BlockNumberToBalance: Convert, BalanceOf>; /// The minimum amount transferred to call `vested_transfer`. #[pallet::constant] - type MinVestedTransfer: Get>; + type MinVestedTransfer: Get>; /// Weight information for extrinsics in this pallet. type WeightInfo: WeightInfo; @@ -204,12 +212,20 @@ pub mod pallet { /// parachain is lagging its block production to avoid clock skew. type BlockNumberProvider: BlockNumberProvider>; + /// The lock identifier to use for the vesting lock on this instance. + /// + /// Each instance must use a distinct `LockIdentifier` so that its currency lock does not + /// interfere with locks from other instances or other pallets. Runtimes may use + /// [`DEFAULT_VESTING_LOCK_ID`] (`*b"vesting "`) for the primary instance and choose a + /// different 8-byte identifier for each additional instance. + type LockId: Get; + /// Maximum number of vesting schedules an account may have at a given moment. const MAX_VESTING_SCHEDULES: u32; } #[pallet::extra_constants] - impl Pallet { + impl, I: 'static> Pallet { #[pallet::constant_name(MaxVestingSchedules)] fn max_vesting_schedules() -> u32 { T::MAX_VESTING_SCHEDULES @@ -217,7 +233,7 @@ pub mod pallet { } #[pallet::hooks] - impl Hooks> for Pallet { + impl, I: 'static> Hooks> for Pallet { fn integrity_test() { assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must be greater than 0"); } @@ -225,35 +241,35 @@ pub mod pallet { /// Information regarding the vesting of a given account. #[pallet::storage] - pub type Vesting = StorageMap< + pub type Vesting, I: 'static = ()> = StorageMap< _, Blake2_128Concat, T::AccountId, - BoundedVec, BlockNumberFor>, MaxVestingSchedulesGet>, + BoundedVec, BlockNumberFor>, MaxVestingSchedulesGet>, >; /// Storage version of the pallet. /// /// New networks start with latest version, as determined by the genesis build. #[pallet::storage] - pub type StorageVersion = StorageValue<_, Releases, ValueQuery>; + pub type StorageVersion, I: 'static = ()> = StorageValue<_, Releases, ValueQuery>; #[pallet::pallet] - pub struct Pallet(_); + pub struct Pallet(_); #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] - pub struct GenesisConfig { - pub vesting: Vec<(T::AccountId, BlockNumberFor, BlockNumberFor, BalanceOf)>, + pub struct GenesisConfig, I: 'static = ()> { + pub vesting: Vec<(T::AccountId, BlockNumberFor, BlockNumberFor, BalanceOf)>, } #[pallet::genesis_build] - impl BuildGenesisConfig for GenesisConfig { + impl, I: 'static> BuildGenesisConfig for GenesisConfig { fn build(&self) { use sp_runtime::traits::Saturating; // Genesis uses the latest storage version. - StorageVersion::::put(Releases::V1); + StorageVersion::::put(Releases::V1); // Generate initial vesting configuration // * who - Account which we are generating vesting configuration for @@ -272,32 +288,32 @@ pub mod pallet { panic!("Invalid VestingInfo params at genesis") }; - Vesting::::try_append(who, vesting_info) + Vesting::::try_append(who, vesting_info) .expect("Too many vesting schedules at genesis."); let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get()); - T::Currency::set_lock(VESTING_ID, who, locked, reasons); + T::Currency::set_lock(T::LockId::get(), who, locked, reasons); } } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { + pub enum Event, I: 'static = ()> { /// A vesting schedule has been created. VestingCreated { account: T::AccountId, schedule_index: u32 }, /// The amount vested has been updated. This could indicate a change in funds available. /// The balance given is the amount which is left unvested (and thus locked). - VestingUpdated { account: T::AccountId, unvested: BalanceOf }, + VestingUpdated { account: T::AccountId, unvested: BalanceOf }, /// An \[account\] has become fully vested. VestingCompleted { account: T::AccountId }, } /// Error for the vesting pallet. #[pallet::error] - pub enum Error { + pub enum Error { /// The account given is not vesting. NotVesting, /// The account already has `MaxVestingSchedules` count of schedules and thus @@ -312,7 +328,7 @@ pub mod pallet { } #[pallet::call] - impl Pallet { + impl, I: 'static> Pallet { /// Unlock any vested funds of the sender account. /// /// The dispatch origin for this call must be _Signed_ and the sender must have funds still @@ -323,8 +339,8 @@ pub mod pallet { /// ## Complexity /// - `O(1)`. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) - .max(T::WeightInfo::vest_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + #[pallet::weight(T::WeightInfo::vest_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) )] pub fn vest(origin: OriginFor) -> DispatchResult { let who = ensure_signed(origin)?; @@ -343,8 +359,8 @@ pub mod pallet { /// ## Complexity /// - `O(1)`. #[pallet::call_index(1)] - #[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) - .max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + #[pallet::weight(T::WeightInfo::vest_other_locked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_other_unlocked(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) )] pub fn vest_other(origin: OriginFor, target: AccountIdLookupOf) -> DispatchResult { ensure_signed(origin)?; @@ -367,12 +383,12 @@ pub mod pallet { /// - `O(1)`. #[pallet::call_index(2)] #[pallet::weight( - T::WeightInfo::vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + T::WeightInfo::vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) )] pub fn vested_transfer( origin: OriginFor, target: AccountIdLookupOf, - schedule: VestingInfo, BlockNumberFor>, + schedule: VestingInfo, BlockNumberFor>, ) -> DispatchResult { let transactor = ensure_signed(origin)?; let target = T::Lookup::lookup(target)?; @@ -395,13 +411,13 @@ pub mod pallet { /// - `O(1)`. #[pallet::call_index(3)] #[pallet::weight( - T::WeightInfo::force_vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + T::WeightInfo::force_vested_transfer(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) )] pub fn force_vested_transfer( origin: OriginFor, source: AccountIdLookupOf, target: AccountIdLookupOf, - schedule: VestingInfo, BlockNumberFor>, + schedule: VestingInfo, BlockNumberFor>, ) -> DispatchResult { ensure_root(origin)?; let target = T::Lookup::lookup(target)?; @@ -432,8 +448,8 @@ pub mod pallet { /// - `schedule2_index`: index of the second schedule to merge. #[pallet::call_index(4)] #[pallet::weight( - T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) - .max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) + T::WeightInfo::not_unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::unlocking_merge_schedules(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES)) )] pub fn merge_schedules( origin: OriginFor, @@ -447,7 +463,7 @@ pub mod pallet { let schedule1_index = schedule1_index as usize; let schedule2_index = schedule2_index as usize; - let schedules = Vesting::::get(&who).ok_or(Error::::NotVesting)?; + let schedules = Vesting::::get(&who).ok_or(Error::::NotVesting)?; let merge_action = VestingAction::Merge { index1: schedule1_index, index2: schedule2_index }; @@ -467,7 +483,7 @@ pub mod pallet { /// - `schedule_index`: The vesting schedule index that should be removed #[pallet::call_index(5)] #[pallet::weight( - T::WeightInfo::force_remove_vesting_schedule(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) + T::WeightInfo::force_remove_vesting_schedule(MaxLocksOf::::get(), T::MAX_VESTING_SCHEDULES) )] pub fn force_remove_vesting_schedule( origin: OriginFor, @@ -477,13 +493,13 @@ pub mod pallet { ensure_root(origin)?; let who = T::Lookup::lookup(target)?; - let schedules_count = Vesting::::decode_len(&who).unwrap_or_default(); - ensure!(schedule_index < schedules_count as u32, Error::::InvalidScheduleParams); + let schedules_count = Vesting::::decode_len(&who).unwrap_or_default(); + ensure!(schedule_index < schedules_count as u32, Error::::InvalidScheduleParams); Self::remove_vesting_schedule(&who, schedule_index)?; Ok(Some(T::WeightInfo::force_remove_vesting_schedule( - MaxLocksOf::::get(), + MaxLocksOf::::get(), schedules_count as u32, )) .into()) @@ -491,22 +507,23 @@ pub mod pallet { } } -impl Pallet { +impl, I: 'static> Pallet { // Public function for accessing vesting storage pub fn vesting( account: T::AccountId, - ) -> Option, BlockNumberFor>, MaxVestingSchedulesGet>> - { - Vesting::::get(account) + ) -> Option< + BoundedVec, BlockNumberFor>, MaxVestingSchedulesGet>, + > { + Vesting::::get(account) } // Create a new `VestingInfo`, based off of two other `VestingInfo`s. // NOTE: We assume both schedules have had funds unlocked up through the current block. fn merge_vesting_info( now: BlockNumberFor, - schedule1: VestingInfo, BlockNumberFor>, - schedule2: VestingInfo, BlockNumberFor>, - ) -> Option, BlockNumberFor>> { + schedule1: VestingInfo, BlockNumberFor>, + schedule2: VestingInfo, BlockNumberFor>, + ) -> Option, BlockNumberFor>> { let schedule1_ending_block = schedule1.ending_block_as_balance::(); let schedule2_ending_block = schedule2.ending_block_as_balance::(); let now_as_balance = T::BlockNumberToBalance::convert(now); @@ -549,16 +566,59 @@ impl Pallet { Some(schedule) } + // Merge two `VestingInfo`s that share the same `starting_block`. For the incoming schedule we + // use `incoming.locked()` (the full original amount) and not `locked_at(now)` so that the + // complete payout is locked regardless of how far into the epoch it arrives. + // + // `per_block` is sized to unlock `target_locked_now` over the remaining time to `ending_block`. + // `locked` is back-calculated so that `locked_at(now) == target_locked_now` exactly. + fn merge_vesting_info_preserving_start( + now: BlockNumberFor, + existing: VestingInfo, BlockNumberFor>, + incoming: VestingInfo, BlockNumberFor>, + ) -> VestingInfo, BlockNumberFor> { + debug_assert_eq!( + existing.starting_block(), + incoming.starting_block(), + "merge_vesting_info_preserving_start: starting_block mismatch" + ); + + // Lock the full incoming amount, irrespective of when the existing schedule started. + let target_locked_now = existing + .locked_at::(now) + .saturating_add(incoming.locked()); + + // The merged schedule should end at the later of the two ending blocks. + let ending_block = existing + .ending_block_as_balance::() + .max(incoming.ending_block_as_balance::()); + + let now_as_balance = T::BlockNumberToBalance::convert(now); + let elapsed = now_as_balance + .saturating_sub(T::BlockNumberToBalance::convert(existing.starting_block())); + let remaining = ending_block.saturating_sub(now_as_balance).max(One::one()); + + // Ceiling division: per_block = ceil(target_locked_now / remaining). + let per_block = (target_locked_now.saturating_add(remaining).saturating_sub(One::one()) / + remaining) + .max(One::one()); + + // Back-calculate "locked" so that "locked_at(now)" exactly matches "target_locked_now". + let locked = target_locked_now.saturating_add(per_block.saturating_mul(elapsed)); + + VestingInfo::new(locked, per_block, existing.starting_block()) + } + // Execute a vested transfer from `source` to `target` with the given `schedule`. fn do_vested_transfer( source: &T::AccountId, target: &T::AccountId, - schedule: VestingInfo, BlockNumberFor>, + schedule: VestingInfo, BlockNumberFor>, ) -> DispatchResult { // Validate user inputs. - ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::::AmountLow); + ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::::AmountLow); if !schedule.is_valid() { - return Err(Error::::InvalidScheduleParams.into()); + return Err(Error::::InvalidScheduleParams.into()); }; // Check we can add to this account prior to any storage writes. @@ -596,14 +656,14 @@ impl Pallet { /// /// NOTE: the amount locked does not include any schedules that are filtered out via `action`. fn report_schedule_updates( - schedules: Vec, BlockNumberFor>>, + schedules: Vec, BlockNumberFor>>, action: VestingAction, - ) -> (Vec, BlockNumberFor>>, BalanceOf) { + ) -> (Vec, BlockNumberFor>>, BalanceOf) { let now = T::BlockNumberProvider::current_block_number(); - let mut total_locked_now: BalanceOf = Zero::zero(); + let mut total_locked_now: BalanceOf = Zero::zero(); let filtered_schedules = action - .pick_schedules::(schedules) + .pick_schedules::(schedules) .filter(|schedule| { let locked_now = schedule.locked_at::(now); let keep = !locked_now.is_zero(); @@ -618,14 +678,14 @@ impl Pallet { } /// Write an accounts updated vesting lock to storage. - fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf) { + fn write_lock(who: &T::AccountId, total_locked_now: BalanceOf) { if total_locked_now.is_zero() { - T::Currency::remove_lock(VESTING_ID, who); - Self::deposit_event(Event::::VestingCompleted { account: who.clone() }); + T::Currency::remove_lock(T::LockId::get(), who); + Self::deposit_event(Event::::VestingCompleted { account: who.clone() }); } else { let reasons = WithdrawReasons::except(T::UnvestedFundsAllowedWithdrawReasons::get()); - T::Currency::set_lock(VESTING_ID, who, total_locked_now, reasons); - Self::deposit_event(Event::::VestingUpdated { + T::Currency::set_lock(T::LockId::get(), who, total_locked_now, reasons); + Self::deposit_event(Event::::VestingUpdated { account: who.clone(), unvested: total_locked_now, }); @@ -635,17 +695,17 @@ impl Pallet { /// Write an accounts updated vesting schedules to storage. fn write_vesting( who: &T::AccountId, - schedules: Vec, BlockNumberFor>>, + schedules: Vec, BlockNumberFor>>, ) -> Result<(), DispatchError> { let schedules: BoundedVec< - VestingInfo, BlockNumberFor>, - MaxVestingSchedulesGet, - > = schedules.try_into().map_err(|_| Error::::AtMaxVestingSchedules)?; + VestingInfo, BlockNumberFor>, + MaxVestingSchedulesGet, + > = schedules.try_into().map_err(|_| Error::::AtMaxVestingSchedules)?; if schedules.len() == 0 { - Vesting::::remove(&who); + Vesting::::remove(&who); } else { - Vesting::::insert(who, schedules) + Vesting::::insert(who, schedules) } Ok(()) @@ -653,7 +713,7 @@ impl Pallet { /// Unlock any vested funds of `who`. fn do_vest(who: T::AccountId) -> DispatchResult { - let schedules = Vesting::::get(&who).ok_or(Error::::NotVesting)?; + let schedules = Vesting::::get(&who).ok_or(Error::::NotVesting)?; let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; @@ -667,15 +727,20 @@ impl Pallet { /// Execute a `VestingAction` against the given `schedules`. Returns the updated schedules /// and locked amount. fn exec_action( - schedules: Vec, BlockNumberFor>>, + schedules: Vec, BlockNumberFor>>, action: VestingAction, - ) -> Result<(Vec, BlockNumberFor>>, BalanceOf), DispatchError> { + ) -> Result< + (Vec, BlockNumberFor>>, BalanceOf), + DispatchError, + > { let (schedules, locked_now) = match action { VestingAction::Merge { index1: idx1, index2: idx2 } => { // The schedule index is based off of the schedule ordering prior to filtering out // any schedules that may be ending at this block. - let schedule1 = *schedules.get(idx1).ok_or(Error::::ScheduleIndexOutOfBounds)?; - let schedule2 = *schedules.get(idx2).ok_or(Error::::ScheduleIndexOutOfBounds)?; + let schedule1 = + *schedules.get(idx1).ok_or(Error::::ScheduleIndexOutOfBounds)?; + let schedule2 = + *schedules.get(idx2).ok_or(Error::::ScheduleIndexOutOfBounds)?; // The length of `schedules` decreases by 2 here since we filter out 2 schedules. // Thus we know below that we can push the new merged schedule without error @@ -709,17 +774,17 @@ impl Pallet { } } -impl frame_support::traits::tokens::VestedPayout> - for Pallet +impl, I: 'static> + frame_support::traits::tokens::VestedPayout> for Pallet where - BalanceOf: MaybeSerializeDeserialize + Debug, + BalanceOf: MaybeSerializeDeserialize + Debug, { type BlockNumber = BlockNumberFor; fn vested_transfer( source: &T::AccountId, dest: &T::AccountId, - amount: BalanceOf, + amount: BalanceOf, duration: BlockNumberFor, start_at: Option>, ) -> DispatchResult { @@ -743,18 +808,61 @@ where Self::do_vested_transfer(source, dest, schedule) } } + + fn add_to_vesting( + source: &T::AccountId, + dest: &T::AccountId, + amount: BalanceOf, + duration: BlockNumberFor, + start_at: BlockNumberFor, + ) -> DispatchResult { + if amount.is_zero() || duration.is_zero() { + return Ok(()); + } + + let now = T::BlockNumberProvider::current_block_number(); + let duration_as_balance = T::BlockNumberToBalance::convert(duration); + let per_block = (amount.saturating_add(duration_as_balance).saturating_sub(One::one()) / + duration_as_balance) + .max(One::one()); + let incoming = VestingInfo::new(amount, per_block, start_at); + let schedules = Vesting::::get(dest).unwrap_or_default(); + + if let Some(idx) = schedules.iter().position(|s| s.starting_block() == start_at) { + // A schedule exists for "start_at", so we merge the incoming schedule with the existing + // one, while intentionally not enforcing "MinVestedTransfer" since per-era amounts + // may be sub-minimum. + T::Currency::transfer(source, dest, amount, ExistenceRequirement::AllowDeath)?; + + // Merge the incoming schedule with the existing one. + let mut schedules = schedules.to_vec(); + schedules[idx] = + Self::merge_vesting_info_preserving_start(now, schedules[idx], incoming); + + // Clean up. + let (schedules, locked_now) = Self::exec_action(schedules, VestingAction::Passive)?; + Self::write_vesting(dest, schedules)?; + Self::write_lock(dest, locked_now); + + Ok(()) + } else { + // No schedule exists for "start_at", so we create a new one, enforcing + // MinVestedTransfer. + Self::do_vested_transfer(source, dest, incoming) + } + } } -impl VestingSchedule for Pallet +impl, I: 'static> VestingSchedule for Pallet where - BalanceOf: MaybeSerializeDeserialize + Debug, + BalanceOf: MaybeSerializeDeserialize + Debug, { type Currency = T::Currency; type Moment = BlockNumberFor; /// Get the amount that is currently being vested and cannot be transferred out of this account. - fn vesting_balance(who: &T::AccountId) -> Option> { - if let Some(v) = Vesting::::get(who) { + fn vesting_balance(who: &T::AccountId) -> Option> { + if let Some(v) = Vesting::::get(who) { let now = T::BlockNumberProvider::current_block_number(); let total_locked_now = v.iter().fold(Zero::zero(), |total, schedule| { schedule.locked_at::(now).saturating_add(total) @@ -779,8 +887,8 @@ where /// NOTE: This doesn't alter the free balance of the account. fn add_vesting_schedule( who: &T::AccountId, - locked: BalanceOf, - per_block: BalanceOf, + locked: BalanceOf, + per_block: BalanceOf, starting_block: BlockNumberFor, ) -> DispatchResult { if locked.is_zero() { @@ -790,18 +898,18 @@ where let vesting_schedule = VestingInfo::new(locked, per_block, starting_block); // Check for `per_block` or `locked` of 0. if !vesting_schedule.is_valid() { - return Err(Error::::InvalidScheduleParams.into()); + return Err(Error::::InvalidScheduleParams.into()); }; - let mut schedules = Vesting::::get(who).unwrap_or_default(); + let mut schedules = Vesting::::get(who).unwrap_or_default(); // NOTE: we must push the new schedule so that `exec_action` // will give the correct new locked amount. - ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::::AtMaxVestingSchedules); + ensure!(schedules.try_push(vesting_schedule).is_ok(), Error::::AtMaxVestingSchedules); debug_assert!(schedules.len() > 0, "schedules cannot be empty after insertion"); let schedule_index = schedules.len() - 1; - Self::deposit_event(Event::::VestingCreated { + Self::deposit_event(Event::::VestingCreated { account: who.clone(), schedule_index: schedule_index as u32, }); @@ -819,18 +927,19 @@ where /// be called prior to `add_vesting_schedule`. fn can_add_vesting_schedule( who: &T::AccountId, - locked: BalanceOf, - per_block: BalanceOf, + locked: BalanceOf, + per_block: BalanceOf, starting_block: BlockNumberFor, ) -> DispatchResult { // Check for `per_block` or `locked` of 0. if !VestingInfo::new(locked, per_block, starting_block).is_valid() { - return Err(Error::::InvalidScheduleParams.into()); + return Err(Error::::InvalidScheduleParams.into()); } ensure!( - (Vesting::::decode_len(who).unwrap_or_default() as u32) < T::MAX_VESTING_SCHEDULES, - Error::::AtMaxVestingSchedules + (Vesting::::decode_len(who).unwrap_or_default() as u32) < + T::MAX_VESTING_SCHEDULES, + Error::::AtMaxVestingSchedules ); Ok(()) @@ -838,7 +947,7 @@ where /// Remove a vesting schedule for a given account. fn remove_vesting_schedule(who: &T::AccountId, schedule_index: u32) -> DispatchResult { - let schedules = Vesting::::get(who).ok_or(Error::::NotVesting)?; + let schedules = Vesting::::get(who).ok_or(Error::::NotVesting)?; let remove_action = VestingAction::Remove { index: schedule_index as usize }; let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), remove_action)?; @@ -851,9 +960,9 @@ where /// An implementation that allows the Vesting Pallet to handle a vested transfer /// on behalf of another Pallet. -impl VestedTransfer for Pallet +impl, I: 'static> VestedTransfer for Pallet where - BalanceOf: MaybeSerializeDeserialize + Debug, + BalanceOf: MaybeSerializeDeserialize + Debug, { type Currency = T::Currency; type Moment = BlockNumberFor; @@ -861,8 +970,8 @@ where fn vested_transfer( source: &T::AccountId, target: &T::AccountId, - locked: BalanceOf, - per_block: BalanceOf, + locked: BalanceOf, + per_block: BalanceOf, starting_block: BlockNumberFor, ) -> DispatchResult { use frame_support::storage::{with_transaction, TransactionOutcome}; diff --git a/substrate/frame/vesting/src/migrations.rs b/substrate/frame/vesting/src/migrations.rs index 33fa5d0df882c..162efc6dacf8f 100644 --- a/substrate/frame/vesting/src/migrations.rs +++ b/substrate/frame/vesting/src/migrations.rs @@ -25,8 +25,8 @@ pub mod v1 { use super::*; #[cfg(feature = "try-runtime")] - pub fn pre_migrate() -> Result<(), &'static str> { - assert!(StorageVersion::::get() == Releases::V0, "Storage version too high."); + pub fn pre_migrate, I: 'static>() -> Result<(), &'static str> { + assert!(StorageVersion::::get() == Releases::V0, "Storage version too high."); log::debug!( target: "runtime::vesting", @@ -38,16 +38,16 @@ pub mod v1 { /// Migrate from single schedule to multi schedule storage. /// WARNING: This migration will delete schedules if `MaxVestingSchedules < 1`. - pub fn migrate() -> Weight { + pub fn migrate, I: 'static>() -> Weight { let mut reads_writes = 0; - Vesting::::translate::, BlockNumberFor>, _>( + Vesting::::translate::, BlockNumberFor>, _>( |_key, vesting_info| { reads_writes += 1; let v: Option< BoundedVec< - VestingInfo, BlockNumberFor>, - MaxVestingSchedulesGet, + VestingInfo, BlockNumberFor>, + MaxVestingSchedulesGet, >, > = vec![vesting_info].try_into().ok(); @@ -66,10 +66,10 @@ pub mod v1 { } #[cfg(feature = "try-runtime")] - pub fn post_migrate() -> Result<(), &'static str> { - assert_eq!(StorageVersion::::get(), Releases::V1); + pub fn post_migrate, I: 'static>() -> Result<(), &'static str> { + assert_eq!(StorageVersion::::get(), Releases::V1); - for (_key, schedules) in Vesting::::iter() { + for (_key, schedules) in Vesting::::iter() { assert!( schedules.len() >= 1, "A bounded vec with incorrect count of items was created." diff --git a/substrate/frame/vesting/src/mock.rs b/substrate/frame/vesting/src/mock.rs index 2efe057658067..d9f015dde352f 100644 --- a/substrate/frame/vesting/src/mock.rs +++ b/substrate/frame/vesting/src/mock.rs @@ -48,6 +48,7 @@ parameter_types! { pub UnvestedFundsAllowedWithdrawReasons: WithdrawReasons = WithdrawReasons::except(WithdrawReasons::TRANSFER | WithdrawReasons::RESERVE); pub static ExistentialDeposit: u64 = 1; + pub const VestingLockId: LockIdentifier = DEFAULT_VESTING_LOCK_ID; } impl Config for Test { type BlockNumberToBalance = Identity; @@ -58,6 +59,7 @@ impl Config for Test { type WeightInfo = (); type UnvestedFundsAllowedWithdrawReasons = UnvestedFundsAllowedWithdrawReasons; type BlockNumberProvider = System; + type LockId = VestingLockId; } pub struct ExtBuilder { diff --git a/substrate/frame/vesting/src/tests.rs b/substrate/frame/vesting/src/tests.rs index e00dd97180b01..cd62ed22fa5ae 100644 --- a/substrate/frame/vesting/src/tests.rs +++ b/substrate/frame/vesting/src/tests.rs @@ -16,7 +16,7 @@ // limitations under the License. use codec::EncodeLike; -use frame_support::{assert_noop, assert_ok, assert_storage_noop}; +use frame_support::{assert_noop, assert_ok, assert_storage_noop, traits::tokens::VestedPayout}; use frame_system::RawOrigin; use sp_runtime::{ traits::{BadOrigin, Identity}, @@ -1265,7 +1265,7 @@ fn vested_transfer_impl_works() { #[test] fn vested_payout_edge_cases() { - use frame_support::{hypothetically, traits::tokens::VestedPayout}; + use frame_support::hypothetically; ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { let alice = 3; let bob = 4; @@ -1331,7 +1331,6 @@ fn vested_payout_edge_cases() { #[test] fn vested_payout_creates_schedule() { - use frame_support::traits::tokens::VestedPayout; ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { let alice = 3; let bob = 4; @@ -1361,7 +1360,6 @@ fn vested_payout_creates_schedule() { #[test] fn vested_payout_self_transfer_creates_schedule() { - use frame_support::traits::tokens::VestedPayout; ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { let alice = 3; let balance_before = Balances::free_balance(&alice); @@ -1380,3 +1378,158 @@ fn vested_payout_self_transfer_creates_schedule() { assert_eq!(schedule[0].locked(), amount); }); } + +#[test] +fn add_to_vesting_zero_shortcuts() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let source = 3u64; + let dest = 4u64; + + // Zero amount: no-op, no schedule created. + assert_ok!(>::add_to_vesting(&source, &dest, 0, 10, 1)); + assert!(VestingStorage::::get(&dest).is_none()); + + // Zero duration: no-op, no schedule created. + assert_ok!(>::add_to_vesting(&source, &dest, 100, 0, 1)); + assert!(VestingStorage::::get(&dest).is_none()); + }); +} + +#[test] +fn add_to_vesting_create_path() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let source = 3u64; + let dest = 4u64; + let amount = ED * 4; // 1024, above MinVestedTransfer (512) + let duration = 20u64; + let start_at = 1u64; + + assert_ok!(>::add_to_vesting( + &source, &dest, amount, duration, start_at + )); + + let schedules = VestingStorage::::get(&dest).unwrap(); + assert_eq!(schedules.len(), 1); + assert_eq!(schedules[0].locked(), amount); + assert_eq!(schedules[0].starting_block(), start_at); + // per_block = ceil(1024/20) = 52 + assert_eq!(schedules[0].per_block(), 52); + }); +} + +#[test] +fn add_to_vesting_create_rejects_sub_min() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let source = 3u64; + let dest = 4u64; + let amount = 100u64; + assert!(amount < ::MinVestedTransfer::get()); + + assert_noop!( + >::add_to_vesting(&source, &dest, amount, 20, 1), + Error::::AmountLow + ); + }); +} + +#[test] +fn add_to_vesting_merge_path_preserves_starting_block_and_accumulates() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let source = 3u64; + let dest = 4u64; + let amount1 = ED * 4; + let amount2 = amount1; + let schedule2_block = 7; + let duration = 20u64; + let start_at = 1u64; + + // First call: create path at block 1. + assert_ok!(>::add_to_vesting( + &source, &dest, amount1, duration, start_at + )); + + // Verify that the schedule was created. + let schedules = VestingStorage::::get(&dest).unwrap(); + assert_eq!(schedules.len(), 1); + assert_eq!(schedules[0].starting_block(), start_at); + + // Advance to second schedule's block, then merge a second payout. + System::set_block_number(schedule2_block); + assert_ok!(>::add_to_vesting( + &source, &dest, amount2, duration, start_at + )); + + // Ensure there is still only one schedule and the starting block was preserved. + let schedules = VestingStorage::::get(&dest).unwrap(); + assert_eq!(schedules.len(), 1); + assert_eq!(schedules[0].starting_block(), start_at); + + // Calculate the expected locked amount after the merge. + let per_block = (amount1 + duration - 1) / duration; + let expected_locked_now = (amount1 - (schedule2_block - start_at) * per_block) + amount2; + + assert_eq!( + schedules[0].locked_at::(schedule2_block), + expected_locked_now + ); + + // Vesting lock on the account reflects the merged amount. + assert_eq!(Vesting::vesting_balance(&dest), Some(expected_locked_now)); + }); +} + +#[test] +fn add_to_vesting_merge_bypasses_min_vested_transfer() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let source = 3u64; + let dest = 4u64; + let start_at = 1u64; + + // Create path first with an above-minimum amount. + assert_ok!(>::add_to_vesting( + &source, + &dest, + ED * 4, + 20, + start_at + )); + + // Merge path with a sub-minimum amount — ensure MinVestedTransfer is not enforced on merge. + let sub_min_amount = 100u64; + assert!(sub_min_amount < ::MinVestedTransfer::get()); + assert_ok!(>::add_to_vesting( + &source, + &dest, + sub_min_amount, + 20, + start_at + )); + + // Schedule count unchanged; merge succeeded. + assert_eq!(VestingStorage::::get(&dest).unwrap().len(), 1); + }); +} + +#[test] +fn add_to_vesting_at_max_schedules_returns_error() { + ExtBuilder::default().existential_deposit(ED).build().execute_with(|| { + let source = 3u64; + let dest = 4u64; + let amount = ED * 4; + + // Mock sets MAX_VESTING_SCHEDULES to 3 and we now fill all 3 slots with distinct + // "start_at" values, so we are at maximum schedule capacity. + for start_at in [1u64, 2u64, 3u64] { + assert_ok!(>::add_to_vesting( + &source, &dest, amount, 20, start_at + )); + } + assert_eq!(VestingStorage::::get(&dest).unwrap().len(), 3); + + // Ensure a created path with a new "start_at" hits the cap. + assert_noop!( + >::add_to_vesting(&source, &dest, amount, 20, 4), + Error::::AtMaxVestingSchedules + ); + }); +} diff --git a/substrate/frame/vesting/src/weights.rs b/substrate/frame/vesting/src/weights.rs index 89c44d14a0611..a7244bef821a1 100644 --- a/substrate/frame/vesting/src/weights.rs +++ b/substrate/frame/vesting/src/weights.rs @@ -81,6 +81,8 @@ pub trait WeightInfo { fn not_unlocking_merge_schedules(l: u32, s: u32, ) -> Weight; fn unlocking_merge_schedules(l: u32, s: u32, ) -> Weight; fn force_remove_vesting_schedule(l: u32, s: u32, ) -> Weight; + fn add_to_vesting_create(l: u32, s: u32, ) -> Weight; + fn add_to_vesting_merge(l: u32, s: u32, ) -> Weight; } /// Weights for `pallet_vesting` using the Substrate node and recommended hardware. @@ -285,6 +287,14 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(4_u64)) .saturating_add(T::DbWeight::get().writes(3_u64)) } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_create(_l: u32, _s: u32) -> Weight { + Weight::zero() + } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_merge(_l: u32, _s: u32) -> Weight { + Weight::zero() + } } // For backwards compatibility and tests. @@ -488,4 +498,12 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(4_u64)) .saturating_add(RocksDbWeight::get().writes(3_u64)) } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_create(_l: u32, _s: u32) -> Weight { + Weight::zero() + } + /// NOTE: placeholder — re-run benchmarks after merging. + fn add_to_vesting_merge(_l: u32, _s: u32) -> Weight { + Weight::zero() + } }