diff --git a/Cargo.lock b/Cargo.lock index 0a29b20c02de4..fea5efbc745a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -983,6 +983,7 @@ dependencies = [ "pallet-asset-rewards", "pallet-assets 29.1.0", "pallet-assets-freezer 0.1.0", + "pallet-assets-vesting", "pallet-aura 27.0.0", "pallet-authorship 28.0.0", "pallet-balances 28.0.0", @@ -1120,6 +1121,7 @@ dependencies = [ "pallet-asset-rewards", "pallet-assets 29.1.0", "pallet-assets-freezer 0.1.0", + "pallet-assets-vesting", "pallet-aura 27.0.0", "pallet-authorship 28.0.0", "pallet-balances 28.0.0", @@ -12162,6 +12164,19 @@ dependencies = [ "sp-runtime 39.0.2", ] +[[package]] +name = "pallet-assets-vesting" +version = "0.1.0" +dependencies = [ + "log", + "pallet-assets 29.1.0", + "pallet-assets-freezer 0.1.0", + "pallet-balances 28.0.0", + "parity-scale-codec", + "polkadot-sdk-frame 0.1.0", + "scale-info", +] + [[package]] name = "pallet-atomic-swap" version = "28.0.0" @@ -18726,6 +18741,7 @@ dependencies = [ "pallet-asset-tx-payment 28.0.0", "pallet-assets 29.1.0", "pallet-assets-freezer 0.1.0", + "pallet-assets-vesting", "pallet-atomic-swap 28.0.0", "pallet-aura 27.0.0", "pallet-authority-discovery 28.0.0", diff --git a/Cargo.toml b/Cargo.toml index 3fa521170c31d..ec928d1cf0422 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -318,6 +318,7 @@ members = [ "substrate/frame/asset-rewards", "substrate/frame/assets", "substrate/frame/assets-freezer", + "substrate/frame/assets-vesting", "substrate/frame/atomic-swap", "substrate/frame/aura", "substrate/frame/authority-discovery", @@ -899,6 +900,7 @@ pallet-asset-rewards = { path = "substrate/frame/asset-rewards", default-feature pallet-asset-tx-payment = { path = "substrate/frame/transaction-payment/asset-tx-payment", default-features = false } pallet-assets = { path = "substrate/frame/assets", default-features = false } pallet-assets-freezer = { path = "substrate/frame/assets-freezer", default-features = false } +pallet-assets-vesting = { path = "substrate/frame/assets-vesting", default-features = false } pallet-atomic-swap = { default-features = false, path = "substrate/frame/atomic-swap" } pallet-aura = { path = "substrate/frame/aura", default-features = false } pallet-authority-discovery = { path = "substrate/frame/authority-discovery", default-features = false } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml index 3da8aa9b6cfea..b46b36398ffa0 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/Cargo.toml @@ -33,6 +33,7 @@ pallet-asset-conversion-tx-payment = { workspace = true } pallet-asset-rewards = { workspace = true } pallet-assets = { workspace = true } pallet-assets-freezer = { workspace = true } +pallet-assets-vesting = { workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } @@ -63,7 +64,11 @@ sp-version = { workspace = true } sp-weights = { workspace = true } # num-traits feature needed for dex integer sq root: -primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true } +primitive-types = { features = [ + "codec", + "num-traits", + "scale-info", +], workspace = true } # Polkadot pallet-xcm = { workspace = true } @@ -126,6 +131,7 @@ runtime-benchmarks = [ "pallet-asset-conversion/runtime-benchmarks", "pallet-asset-rewards/runtime-benchmarks", "pallet-assets-freezer/runtime-benchmarks", + "pallet-assets-vesting/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -166,6 +172,7 @@ try-runtime = [ "pallet-asset-conversion/try-runtime", "pallet-asset-rewards/try-runtime", "pallet-assets-freezer/try-runtime", + "pallet-assets-vesting/try-runtime", "pallet-assets/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", @@ -217,6 +224,7 @@ std = [ "pallet-asset-conversion/std", "pallet-asset-rewards/std", "pallet-assets-freezer/std", + "pallet-assets-vesting/std", "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index 43b7bf0ba1184..7233950699800 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -113,7 +113,7 @@ use xcm_runtime_apis::{ #[cfg(feature = "runtime-benchmarks")] use frame_support::traits::PalletInfoAccess; - +use sp_runtime::traits::ConvertInto; use weights::{BlockExecutionWeight, ExtrinsicBaseWeight, RocksDbWeight}; impl_opaque_keys! { @@ -290,6 +290,25 @@ impl pallet_assets_freezer::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +parameter_types! { + pub const TrustBackedMinVestedTransfer: Balance = 100 * CENTS; +} + +pub type TrustBackedAssetsVestingInstance = pallet_assets_vesting::Instance1; + +impl pallet_assets_vesting::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ForceOrigin = EnsureRoot; + type Assets = Assets; + type Freezer = AssetsFreezer; + type BlockNumberToBalance = ConvertInto; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); + type MinVestedTransfer = TrustBackedMinVestedTransfer; + type BlockNumberProvider = System; + const MAX_VESTING_SCHEDULES: u32 = 28; +} + parameter_types! { pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); @@ -511,6 +530,25 @@ impl pallet_assets_freezer::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +parameter_types! { + pub const ForeignMinVestedTransfer: Balance = 1; +} + +pub type ForeignAssetsVestingInstance = pallet_assets_vesting::Instance2; + +impl pallet_assets_vesting::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ForceOrigin = EnsureRoot; + type Assets = ForeignAssets; + type Freezer = ForeignAssetsFreezer; + type BlockNumberToBalance = ConvertInto; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); + type MinVestedTransfer = ForeignMinVestedTransfer; + type BlockNumberProvider = System; + const MAX_VESTING_SCHEDULES: u32 = 28; +} + parameter_types! { // One storage item; key size is 32; value is size 4+4+16+32 bytes = 56 bytes. pub const DepositBase: Balance = deposit(1, 88); @@ -1107,6 +1145,9 @@ construct_runtime!( ForeignAssetsFreezer: pallet_assets_freezer:: = 58, PoolAssetsFreezer: pallet_assets_freezer:: = 59, + AssetsVesting: pallet_assets_vesting:: = 80, + ForeignAssetsVesting: pallet_assets_vesting:: = 81, + AssetRewards: pallet_asset_rewards = 60, // TODO: the pallet instance should be removed once all pools have migrated @@ -1299,6 +1340,8 @@ mod benches { [pallet_assets, Local] [pallet_assets, Foreign] [pallet_assets, Pool] + [pallet_assets_vesting, LocalVesting] + [pallet_assets_vesting, ForeignVesting] [pallet_asset_conversion, AssetConversion] [pallet_asset_rewards, AssetRewards] [pallet_asset_conversion_tx_payment, AssetTxPayment] @@ -1670,6 +1713,9 @@ impl_runtime_apis! { type Foreign = pallet_assets::Pallet::; type Pool = pallet_assets::Pallet::; + type LocalVesting = pallet_assets::Pallet::; + type ForeignVesting = pallet_assets::Pallet::; + type ToWestend = XcmBridgeHubRouterBench; let mut list = Vec::::new(); @@ -1975,6 +2021,9 @@ impl_runtime_apis! { type Foreign = pallet_assets::Pallet::; type Pool = pallet_assets::Pallet::; + type LocalVesting = pallet_assets::Pallet::; + type ForeignVesting = pallet_assets::Pallet::; + type ToWestend = XcmBridgeHubRouterBench; use frame_support::traits::WhitelistedStorageKeys; diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml index f7fb858de62e8..8a338801191cd 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/Cargo.toml @@ -33,6 +33,7 @@ pallet-asset-conversion-tx-payment = { workspace = true } pallet-asset-rewards = { workspace = true } pallet-assets = { workspace = true } pallet-assets-freezer = { workspace = true } +pallet-assets-vesting = { workspace = true } pallet-aura = { workspace = true } pallet-authorship = { workspace = true } pallet-balances = { workspace = true } @@ -66,7 +67,11 @@ sp-transaction-pool = { workspace = true } sp-version = { workspace = true } # num-traits feature needed for dex integer sq root: -primitive-types = { features = ["codec", "num-traits", "scale-info"], workspace = true } +primitive-types = { features = [ + "codec", + "num-traits", + "scale-info", +], workspace = true } # Polkadot pallet-xcm = { workspace = true } @@ -130,6 +135,7 @@ runtime-benchmarks = [ "pallet-asset-conversion/runtime-benchmarks", "pallet-asset-rewards/runtime-benchmarks", "pallet-assets-freezer/runtime-benchmarks", + "pallet-assets-vesting/runtime-benchmarks", "pallet-assets/runtime-benchmarks", "pallet-balances/runtime-benchmarks", "pallet-collator-selection/runtime-benchmarks", @@ -173,6 +179,7 @@ try-runtime = [ "pallet-asset-conversion/try-runtime", "pallet-asset-rewards/try-runtime", "pallet-assets-freezer/try-runtime", + "pallet-assets-vesting/try-runtime", "pallet-assets/try-runtime", "pallet-aura/try-runtime", "pallet-authorship/try-runtime", @@ -227,6 +234,7 @@ std = [ "pallet-asset-conversion/std", "pallet-asset-rewards/std", "pallet-assets-freezer/std", + "pallet-assets-vesting/std", "pallet-assets/std", "pallet-aura/std", "pallet-authorship/std", 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 6e0fa19320dd2..0ef6143cbc076 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -105,7 +105,7 @@ use xcm::{ #[cfg(feature = "runtime-benchmarks")] use frame_support::traits::PalletInfoAccess; - +use sp_runtime::traits::ConvertInto; #[cfg(feature = "runtime-benchmarks")] use xcm::latest::prelude::{ Asset, Assets as XcmAssets, Fungible, Here, InteriorLocation, Junction, Junction::*, Location, @@ -294,6 +294,25 @@ impl pallet_assets_freezer::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +parameter_types! { + pub const TrustBackedMinVestedTransfer: Balance = 100 * CENTS; +} + +pub type TrustBackedAssetsVestingInstance = pallet_assets_vesting::Instance1; + +impl pallet_assets_vesting::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ForceOrigin = EnsureRoot; + type Assets = Assets; + type Freezer = AssetsFreezer; + type BlockNumberToBalance = ConvertInto; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); + type MinVestedTransfer = TrustBackedMinVestedTransfer; + type BlockNumberProvider = System; + const MAX_VESTING_SCHEDULES: u32 = 28; +} + parameter_types! { pub const AssetConversionPalletId: PalletId = PalletId(*b"py/ascon"); pub const LiquidityWithdrawalFee: Permill = Permill::from_percent(0); @@ -563,6 +582,25 @@ impl pallet_assets_freezer::Config for Runtime { type RuntimeEvent = RuntimeEvent; } +parameter_types! { + pub const ForeignMinVestedTransfer: Balance = 1; +} + +pub type ForeignAssetsVestingInstance = pallet_assets_vesting::Instance2; + +impl pallet_assets_vesting::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type ForceOrigin = EnsureRoot; + type Assets = ForeignAssets; + type Freezer = ForeignAssetsFreezer; + type BlockNumberToBalance = ConvertInto; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); + type MinVestedTransfer = ForeignMinVestedTransfer; + type BlockNumberProvider = System; + const MAX_VESTING_SCHEDULES: u32 = 28; +} + parameter_types! { // One storage item; key size is 32; value is size 4+4+16+32 bytes = 56 bytes. pub const DepositBase: Balance = deposit(1, 88); @@ -1168,6 +1206,10 @@ construct_runtime!( AssetsFreezer: pallet_assets_freezer:: = 57, ForeignAssetsFreezer: pallet_assets_freezer:: = 58, PoolAssetsFreezer: pallet_assets_freezer:: = 59, + + AssetsVesting: pallet_assets_vesting:: = 80, + ForeignAssetsVesting: pallet_assets_vesting:: = 81, + Revive: pallet_revive = 60, AssetRewards: pallet_asset_rewards = 61, @@ -1448,6 +1490,8 @@ mod benches { [pallet_assets, Local] [pallet_assets, Foreign] [pallet_assets, Pool] + [pallet_assets_vesting, LocalVesting] + [pallet_assets_vesting, ForeignVesting] [pallet_asset_conversion, AssetConversion] [pallet_asset_rewards, AssetRewards] [pallet_asset_conversion_tx_payment, AssetTxPayment] @@ -1866,6 +1910,9 @@ impl_runtime_apis! { type Foreign = pallet_assets::Pallet::; type Pool = pallet_assets::Pallet::; + type LocalVesting = pallet_assets::Pallet::; + type ForeignVesting = pallet_assets::Pallet::; + type ToRococo = XcmBridgeHubRouterBench; let mut list = Vec::::new(); @@ -2171,6 +2218,9 @@ impl_runtime_apis! { type Foreign = pallet_assets::Pallet::; type Pool = pallet_assets::Pallet::; + type LocalVesting = pallet_assets::Pallet::; + type ForeignVesting = pallet_assets::Pallet::; + type ToRococo = XcmBridgeHubRouterBench; use frame_support::traits::WhitelistedStorageKeys; diff --git a/prdoc/pr_7404.prdoc b/prdoc/pr_7404.prdoc new file mode 100644 index 0000000000000..97226ee388150 --- /dev/null +++ b/prdoc/pr_7404.prdoc @@ -0,0 +1,16 @@ +title: Bridges small nits/improvements +doc: +- audience: Runtime Dev + description: | + This Pull Request introduces `pallet-assets-vesting`, allowing runtimes to add vesting schedules to assets. +crates: +- name: asset-hub-rococo-runtime + bump: major +- name: asset-hub-westend-runtime + bump: major +- name: frame-support + bump: minor +- name: pallet-assets-vesting + bump: patch +- name: polkadot-sdk + bump: minor diff --git a/substrate/frame/assets-vesting/Cargo.toml b/substrate/frame/assets-vesting/Cargo.toml new file mode 100644 index 0000000000000..428b2c520e477 --- /dev/null +++ b/substrate/frame/assets-vesting/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "pallet-assets-vesting" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "Apache-2.0" +homepage.workspace = true +repository.workspace = true +description = "FRAME pallet for manage vesting on Assets" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true } +frame = { workspace = true, features = ["runtime"] } +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } + +[dev-dependencies] +pallet-assets = { workspace = true } +pallet-assets-freezer = { workspace = true } +pallet-balances = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame/std", + "log/std", + "pallet-assets-freezer/std", + "pallet-assets/std", + "pallet-balances/std", + "scale-info/std", +] +runtime-benchmarks = [ + "frame/runtime-benchmarks", + "pallet-assets-freezer/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", +] +try-runtime = [ + "frame/try-runtime", + "pallet-assets-freezer/try-runtime", + "pallet-assets/try-runtime", + "pallet-balances/try-runtime", +] diff --git a/substrate/frame/assets-vesting/src/benchmarking.rs b/substrate/frame/assets-vesting/src/benchmarking.rs new file mode 100644 index 0000000000000..da5596fddfd3f --- /dev/null +++ b/substrate/frame/assets-vesting/src/benchmarking.rs @@ -0,0 +1,417 @@ +// This file is part of Substrate. + +// 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. + +//! Assets vesting pallet benchmarking. + +use crate::*; +use frame::benchmarking::prelude::*; + +const SEED: u32 = 0; + +fn create_asset, I: 'static>() -> Result, DispatchError> +where + AssetIdOf: Zero, +{ + let id = AssetIdOf::::zero(); + let admin = account::>("admin", 0, SEED); + + T::Assets::create(id.clone(), admin, true, 1u32.into())?; + Ok(id) +} + +fn initialize_asset_account_with_balance, I: 'static>( + id: AssetIdOf, + who: &AccountIdOf, + balance: BalanceOf, +) -> BalanceOf { + T::Assets::set_balance(id, &who, balance) +} + +fn initialize_asset_account, I: 'static>( + id: AssetIdOf, + who: &AccountIdOf, +) -> BalanceOf { + let min_balance = T::Assets::minimum_balance(id.clone()); + initialize_asset_account_with_balance::(id, who, min_balance) +} + +fn add_vesting_schedules, I: 'static>( + id: AssetIdOf, + target: &AccountIdOf, + n: u32, +) -> Result, &'static str> { + let min_balance = T::Assets::minimum_balance(id.clone()); + let min_transfer = T::MinVestedTransfer::get().max(min_balance); + let locked = min_transfer.checked_mul(&20_u32.into()).unwrap(); + + let source = account::>("source", 0, SEED); + initialize_asset_account_with_balance::( + id.clone(), + &source, + min_balance + locked.checked_mul(&n.into()).unwrap(), + ); + + // Schedule has a duration of 20. + let per_block = min_transfer; + let starting_block = 1_u32; + + T::BlockNumberProvider::set_block_number(BlockNumberFor::::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()); + Pallet::::do_vested_transfer(id.clone(), &source, target, schedule)?; + } + + Ok(total_locked) +} + +#[instance_benchmarks( +where + AssetIdOf: Zero +)] +mod benchmarks { + use super::*; + use frame::traits::tokens::Preservation::Preserve; + + #[benchmark] + fn vest_locked(s: Linear<1, T::MAX_VESTING_SCHEDULES>) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `caller` and add vesting schedules. + let caller = whitelisted_caller(); + initialize_asset_account::(id.clone(), &caller); + let expected_balance = add_vesting_schedules::(id.clone(), &caller, s)?; + + // At block zero, everything is vested. + assert_eq!(frame_system::Pallet::::block_number(), BlockNumberFor::::zero()); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(expected_balance), + "Vesting schedule not added", + ); + + #[extrinsic_call] + vest(RawOrigin::Signed(caller.clone()), id.clone()); + + // Nothing happened since everything is still vested. + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(expected_balance), + "Vesting schedule was removed", + ); + + Ok(()) + } + + #[benchmark] + fn vest_unlocked(s: Linear<1, T::MAX_VESTING_SCHEDULES>) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `caller` and add vesting schedules. + let caller = whitelisted_caller(); + initialize_asset_account::(id.clone(), &caller); + add_vesting_schedules::(id.clone(), &caller, s)?; + + // At block 21, everything is unlocked. + T::BlockNumberProvider::set_block_number(21_u32.into()); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(BalanceOf::::zero()), + "Vesting schedule still active", + ); + + #[extrinsic_call] + vest(RawOrigin::Signed(caller.clone()), id.clone()); + + // Vesting schedule is removed! + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + None, + "Vesting schedule was not removed", + ); + + Ok(()) + } + + #[benchmark] + fn vest_other_locked(s: Linear<1, T::MAX_VESTING_SCHEDULES>) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `other` and add vesting schedules. + let other = account::>("other", 0, SEED); + initialize_asset_account::(id.clone(), &other); + let expected_balance = add_vesting_schedules::(id.clone(), &other, s)?; + + // At block zero, everything is vested. + assert_eq!(frame_system::Pallet::::block_number(), BlockNumberFor::::zero()); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &other), + Some(expected_balance), + "Vesting schedule not added", + ); + + let caller = whitelisted_caller::>(); + let other_lookup = T::Lookup::unlookup(other.clone()); + + #[extrinsic_call] + vest_other(RawOrigin::Signed(caller.clone()), id.clone(), other_lookup); + + // Nothing happened since everything is still vested. + assert_eq!( + Pallet::::vesting_balance(id.clone(), &other), + Some(expected_balance), + "Vesting schedule was removed", + ); + + Ok(()) + } + + #[benchmark] + fn vest_other_unlocked(s: Linear<1, T::MAX_VESTING_SCHEDULES>) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `other` and add vesting schedules. + let other = account::>("other", 0, SEED); + initialize_asset_account::(id.clone(), &other); + add_vesting_schedules::(id.clone(), &other, s)?; + + // At block 21 everything is unlocked. + T::BlockNumberProvider::set_block_number(21_u32.into()); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &other), + Some(BalanceOf::::zero()), + "Vesting schedule still active", + ); + + let caller = whitelisted_caller::(); + let other_lookup = T::Lookup::unlookup(other.clone()); + + #[extrinsic_call] + vest_other(RawOrigin::Signed(caller.clone()), id.clone(), other_lookup); + + // Vesting schedule is removed. + assert_eq!( + Pallet::::vesting_balance(id.clone(), &other), + None, + "Vesting schedule was not removed", + ); + + Ok(()) + } + + #[benchmark] + fn force_vested_transfer( + s: Linear<0, { T::MAX_VESTING_SCHEDULES - 1 }>, + ) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `source` with max balance. + let source = account::>("transfer_source", 0, SEED); + initialize_asset_account_with_balance::( + id.clone(), + &source, + T::Assets::minimum_balance(id.clone()) + T::MinVestedTransfer::get(), + ); + + // Initialize `target`. + let target = account::>("target", 0, SEED); + initialize_asset_account::(id.clone(), &target); + + // Add one less than max vesting schedules. + let orig_balance = T::Assets::total_balance(id.clone(), &target); + let mut expected_balance = add_vesting_schedules::(id.clone(), &target, s)?; + + // Prepare schedule vested transfer of `MinVestedTransfer` across 20 blocks. + let transfer_amount = T::MinVestedTransfer::get(); + let per_block = transfer_amount.checked_div(&20_u32.into()).unwrap(); + expected_balance += transfer_amount; + + let source_lookup = T::Lookup::unlookup(source.clone()); + let target_lookup = T::Lookup::unlookup(target.clone()); + let vesting_schedule = VestingInfo::new(transfer_amount, per_block, 1_u32.into()); + + #[extrinsic_call] + _(RawOrigin::Root, id.clone(), source_lookup, target_lookup, vesting_schedule); + + assert_eq!( + orig_balance + expected_balance, + T::Assets::total_balance(id.clone(), &target), + "Transfer didn't happen", + ); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &target), + Some(expected_balance), + "Lock not correctly updated", + ); + + Ok(()) + } + + #[benchmark] + fn not_unlocking_merge_schedules( + s: Linear<2, { T::MAX_VESTING_SCHEDULES }>, + ) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `caller` and add vesting schedules. + let caller = whitelisted_caller::>(); + initialize_asset_account::(id.clone(), &caller); + let expected_balance = add_vesting_schedules::(id.clone(), &caller, s)?; + + // Schedules are not vesting at block 0. + assert_eq!(frame_system::Pallet::::block_number(), BlockNumberFor::::zero()); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(expected_balance), + "Vesting balance should equal sum locked of all schedules", + ); + assert_eq!( + Vesting::::get(id.clone(), &caller).unwrap().len(), + s as usize, + "There should be exactly max vesting schedules" + ); + + #[extrinsic_call] + merge_schedules(RawOrigin::Signed(caller.clone()), id.clone(), 0, s - 1); + + let expected_schedule = VestingInfo::new( + T::MinVestedTransfer::get() * 20_u32.into() * 2_u32.into(), + T::MinVestedTransfer::get() * 2_u32.into(), + 1_u32.into(), + ); + let expected_index = (s - 2) as usize; + assert_eq!( + Vesting::::get(id.clone(), &caller).unwrap()[expected_index], + expected_schedule + ); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(expected_balance), + "Vesting balance should equal total locked of all schedules", + ); + assert_eq!( + Vesting::::get(id.clone(), &caller).unwrap().len(), + (s - 1) as usize, + "Schedule count should reduce by 1" + ); + + Ok(()) + } + + #[benchmark] + fn unlocking_merge_schedules( + s: Linear<2, { T::MAX_VESTING_SCHEDULES }>, + ) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Destination used just for transfers in asserts. + let test_dest: AccountIdOf = account("test_dest", 0, SEED); + + // Initialize `caller` and add vesting schedules. + let caller = whitelisted_caller::>(); + initialize_asset_account::(id.clone(), &caller); + let total_transferred = add_vesting_schedules::(id.clone(), &caller, s)?; + + // Go to about halfway through all the schedules' duration. (They all start at 1, and have a + // duration of 20 or 21). + T::BlockNumberProvider::set_block_number(11_u32.into()); + // We expect half the original locked balance (+ any remainder that vests on the last + // block). + let expected_balance = total_transferred / 2_u32.into(); + + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(expected_balance), + "Vesting balance should reflect that we are half way through all schedules duration", + ); + assert_eq!( + Vesting::::get(id.clone(), &caller).unwrap().len(), + s as usize, + "There should be exactly max vesting schedules" + ); + + // The balance is not actually transferable because it has not been unlocked. + assert!(T::Assets::transfer(id.clone(), &caller, &test_dest, expected_balance, Preserve) + .is_err()); + + #[extrinsic_call] + merge_schedules(RawOrigin::Signed(caller.clone()), id.clone(), 0, s - 1); + + let expected_schedule = VestingInfo::new( + T::MinVestedTransfer::get() * 2_u32.into() * 10_u32.into(), + T::MinVestedTransfer::get() * 2_u32.into(), + 11_u32.into(), + ); + let expected_index = (s - 2) as usize; + assert_eq!( + Vesting::::get(id.clone(), &caller).unwrap()[expected_index], + expected_schedule, + "New schedule is properly created and placed" + ); + assert_eq!( + Pallet::::vesting_balance(id.clone(), &caller), + Some(expected_balance), + "Vesting balance should equal half total locked of all schedules", + ); + assert_eq!( + Vesting::::get(id.clone(), &caller).unwrap().len(), + (s - 1) as usize, + "Schedule count should reduce by 1" + ); + // Since merge unlocks all schedules we can now transfer the balance. + T::Assets::transfer(id, &caller, &test_dest, expected_balance, Preserve)?; + + Ok(()) + } + + #[benchmark] + fn force_remove_vesting_schedule( + s: Linear<2, { T::MAX_VESTING_SCHEDULES }>, + ) -> Result<(), BenchmarkError> { + let id = create_asset::()?; + + // Initialize `caller` and add vesting schedules. + let target = account::>("target", 0, SEED); + initialize_asset_account::(id.clone(), &target); + add_vesting_schedules::(id.clone(), &target, s)?; + + // The last vesting schedule. + let schedule_index = s - 1; + + let target_lookup = T::Lookup::unlookup(target.clone()); + + #[extrinsic_call] + _(RawOrigin::Root, id.clone(), target_lookup, schedule_index); + + assert_eq!( + Vesting::::get(id.clone(), &target).unwrap().len(), + schedule_index as usize, + "Schedule count should reduce by 1" + ); + + Ok(()) + } + + impl_benchmark_test_suite! { + Pallet, + frame::testing_prelude::TestExternalities::default(), + mock::Test + } +} diff --git a/substrate/frame/assets-vesting/src/lib.rs b/substrate/frame/assets-vesting/src/lib.rs new file mode 100644 index 0000000000000..0ef23f73db55a --- /dev/null +++ b/substrate/frame/assets-vesting/src/lib.rs @@ -0,0 +1,775 @@ +// This file is part of Substrate. + +// 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. + +//! # Assets Vesting Pallet +//! +//! - [`Config`] +//! - [`Call`] +//! +//! ## Overview +//! +//! A simple pallet providing a means of placing a linear curve on an assets account's frozen +//! balance. This pallet ensures that there is a frozen amount in place preventing the balance to +//! drop below the *unvested* amount. +//! +//! As the vested amount increases over time, the unvested amount reduces. However, freezes remain +//! in place and an explicit action is needed on behalf of the user to ensure that the frozen +//! amount is equivalent to the amount remaining to be vested. This is done through a dispatchable +//! function, either `vest` (in typical case where the sender is calling on their own behalf) or +//! `vest_other` in case the sender is calling on another account's behalf. +//! +//! ## Interface +//! +//! This pallet implements the `VestingSchedule` trait. +//! +//! ### Dispatchable Functions +//! +//! - `vest` - Update the lock, reducing it in line with the amount "vested" so far. +//! - `vest_other` - Update the lock of another account, reducing it in line with the amount +//! "vested" so far. + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "runtime-benchmarks")] +mod benchmarking; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +mod types; +mod weights; + +use frame::{ + deps::codec::Decode, + prelude::*, + traits::{ + fungibles::{Inspect, Mutate, MutateFreeze, VestedTransfer, VestingSchedule}, + tokens::Preservation, + }, +}; +use scale_info::prelude::vec::Vec; + +pub use pallet::*; +pub use types::*; +pub use weights::*; + +#[cfg(feature = "runtime-benchmarks")] +use frame::traits::fungibles::Create; + +#[frame::pallet] +pub mod pallet { + use super::*; + + #[pallet::config] + pub trait Config: frame_system::Config { + /// The overarching event type. + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + + /// An Origin that can control the `force` calls. + type ForceOrigin: EnsureOrigin; + + /// Type represents interactions between assets + #[cfg(not(feature = "runtime-benchmarks"))] + type Assets: Inspect> + Mutate>; + + /// Type represents interactions between assets + #[cfg(feature = "runtime-benchmarks")] + type Assets: Inspect> + + Mutate> + + Create>; + + /// Type allows handling fungibles' freezes. + type Freezer: Inspect, AssetId = AssetIdOf, Balance = BalanceOf> + + MutateFreeze< + AccountIdOf, + Id = Self::RuntimeFreezeReason, + AssetId = AssetIdOf, + Balance = BalanceOf, + >; + + /// Convert the block number into a balance. + type BlockNumberToBalance: Convert, BalanceOf>; + + /// The overarching freeze reason. + type RuntimeFreezeReason: From>; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + + /// The minimum amount transferred to call `vested_transfer`. + #[pallet::constant] + type MinVestedTransfer: Get>; + + /// Provider for the block number. + type BlockNumberProvider: BlockNumberProvider>; + + /// Maximum number of vesting schedules an account may have at a given moment. + const MAX_VESTING_SCHEDULES: u32; + } + + #[pallet::extra_constants] + impl, I: 'static> Pallet { + #[pallet::constant_name(MaxVestingSchedules)] + fn max_vesting_schedules() -> u32 { + T::MAX_VESTING_SCHEDULES + } + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::genesis_config] + #[derive(DefaultNoBound)] + pub struct GenesisConfig, I: 'static = ()> { + pub vesting: + Vec<(Vec, T::AccountId, BlockNumberFor, BlockNumberFor, BalanceOf)>, + } + + #[pallet::genesis_build] + impl, I: 'static> BuildGenesisConfig for GenesisConfig { + fn build(&self) { + // Generate initial vesting configuration + // * asset - The id of the asset class the vesting is related to. + // * who - Account which we are generating vesting configuration for + // * begin - Block when the account will start to vest + // * length - Number of blocks from `begin` until fully vested + // * liquid - Number of units which can be spent before vesting begins + for &(ref id, ref who, begin, length, liquid) in self.vesting.iter() { + let asset = AssetIdOf::::decode(&mut TrailingZeroInput::new(id)) + .expect("Invalid AssetId encoding"); + let balance = T::Assets::total_balance(asset.clone(), who); + assert!(!balance.is_zero(), "Assets must be init'd before vesting"); + + // Total genesis `balance` minus `liquid` equals assets frozen for vesting + let frozen = balance.saturating_sub(liquid); + let length_as_balance = T::BlockNumberToBalance::convert(length); + let per_block = frozen / length_as_balance.max(One::one()); + + let vesting_info = VestingInfo::new(frozen, per_block, begin); + if !vesting_info.is_valid() { + panic!("Invalid VestingInfo params at genesis") + }; + + Vesting::::try_append(asset.clone(), who, vesting_info) + .expect("Too many vesting schedules at genesis."); + + T::Freezer::set_freeze(asset, &FreezeReason::::Vesting.into(), who, frozen) + .expect("Too many freezes at genesis"); + } + } + } + + /// Information regarding the vesting of a given account. + #[pallet::storage] + pub type Vesting, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + AssetIdOf, + Blake2_128Concat, + T::AccountId, + BoundedVec, BlockNumberFor>, MaxVestingSchedulesGet>, + >; + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + fn integrity_test() { + assert!(T::MAX_VESTING_SCHEDULES > 0, "`MaxVestingSchedules` must ge greater than 0"); + } + } + + /// A reason for the pallet assets vesting placing a freeze on funds. + #[pallet::composite_enum] + pub enum FreezeReason { + // An account is vesting some funds. + Vesting, + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// 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 frozen). + VestingUpdated { asset: AssetIdOf, account: T::AccountId, unvested: BalanceOf }, + /// An \[asset account\] has become fully vested. + VestingCompleted { asset: AssetIdOf, account: T::AccountId }, + } + + /// Error for the vesting pallet. + #[pallet::error] + pub enum Error { + /// The account given is not vesting. + NotVesting, + /// The account already has `MaxVestingSchedules` count of schedules and thus + /// cannot add another one. Consider merging existing schedules in order to add another. + AtMaxVestingSchedules, + /// Amount being transferred is too low to create a vesting schedule. + AmountLow, + /// An index was out of bounds of the vesting schedules. + ScheduleIndexOutOfBounds, + /// Failed to create a new schedule because some parameter was invalid. + InvalidScheduleParams, + } + + #[pallet::call] + 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 + /// frozen under this pallet. + /// + /// - `asset`: Id of the asset class of the asset account for which the vesting applies. + /// + /// Emits either `VestingCompleted` or `VestingUpdated`. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(0)] + #[pallet::weight(T::WeightInfo::vest_locked(T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_unlocked(T::MAX_VESTING_SCHEDULES)) + )] + pub fn vest(origin: OriginFor, asset: AssetIdOf) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::do_vest(asset, who) + } + + /// Unlock any vested funds of a `target` account. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `asset`: Id of the asset class of the asset account for which the vesting applies. + /// - `target`: The account whose vested funds should be unlocked. Must have funds still + /// frozen under this pallet. + /// + /// Emits either `VestingCompleted` or `VestingUpdated`. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(1)] + #[pallet::weight(T::WeightInfo::vest_other_locked(T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::vest_other_unlocked(T::MAX_VESTING_SCHEDULES)) + )] + pub fn vest_other( + origin: OriginFor, + asset: AssetIdOf, + target: AccountIdLookupOf, + ) -> DispatchResult { + ensure_signed(origin)?; + let who = T::Lookup::lookup(target)?; + Self::do_vest(asset, who) + } + + /// Create a vested transfer. + /// + /// The dispatch origin for this call must be _Signed_. + /// + /// - `asset`: Id of the asset class of the asset account for which the vesting applies. + /// - `target`: The account receiving the vested funds. + /// - `schedule`: The vesting schedule attached to the transfer. + /// + /// Emits `VestingCreated`. + /// + /// NOTE: This will unlock all schedules through the current block. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(2)] + #[pallet::weight(T::WeightInfo::vested_transfer(T::MAX_VESTING_SCHEDULES))] + pub fn vested_transfer( + origin: OriginFor, + asset: AssetIdOf, + target: AccountIdLookupOf, + schedule: VestingInfo, BlockNumberFor>, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let target = T::Lookup::lookup(target)?; + Self::do_vested_transfer(asset, &transactor, &target, schedule) + } + + /// Force a vested transfer. + /// + /// The dispatch origin for this call must be `ForceOrigin`. + /// + /// - `asset`: Id of the asset class of the asset account for which the vesting applies. + /// - `source`: The account whose funds should be transferred. + /// - `target`: The account that should be transferred the vested funds. + /// - `schedule`: The vesting schedule attached to the transfer. + /// + /// Emits `VestingCreated`. + /// + /// NOTE: This will unlock all schedules through the current block. + /// + /// ## Complexity + /// - `O(1)`. + #[pallet::call_index(3)] + #[pallet::weight(T::WeightInfo::force_vested_transfer(T::MAX_VESTING_SCHEDULES))] + pub fn force_vested_transfer( + origin: OriginFor, + asset: AssetIdOf, + source: AccountIdLookupOf, + target: AccountIdLookupOf, + schedule: VestingInfo, BlockNumberFor>, + ) -> DispatchResult { + ensure_root(origin)?; + let target = T::Lookup::lookup(target)?; + let source = T::Lookup::lookup(source)?; + Self::do_vested_transfer(asset, &source, &target, schedule) + } + + /// Merge two vesting schedules together, creating a new vesting schedule that unlocks over + /// the highest possible start and end blocks. If both schedules have already started the + /// current block will be used as the schedule start; with the caveat that if one schedule + /// is finished by the current block, the other will be treated as the new merged schedule, + /// unmodified. + /// + /// NOTE: If `schedule1_index == schedule2_index` this is a no-op. + /// NOTE: This will unlock all schedules through the current block prior to merging. + /// NOTE: If both schedules have ended by the current block, no new schedule will be created + /// and both will be removed. + /// + /// Merged schedule attributes: + /// - `starting_block`: `MAX(schedule1.starting_block, scheduled2.starting_block, + /// current_block)`. + /// - `ending_block`: `MAX(schedule1.ending_block, schedule2.ending_block)`. + /// - `locked`: `schedule1.locked_at(current_block) + schedule2.locked_at(current_block)`. + /// The dispatch origin for this call must be _Signed_. + /// + /// - `asset`: Id of the asset class of the asset account for which the vesting applies. + /// - `schedule1_index`: index of the first schedule to merge. + /// - `schedule2_index`: index of the second schedule to merge. + #[pallet::call_index(4)] + #[pallet::weight( + T::WeightInfo::not_unlocking_merge_schedules(T::MAX_VESTING_SCHEDULES) + .max(T::WeightInfo::unlocking_merge_schedules(T::MAX_VESTING_SCHEDULES)) + )] + pub fn merge_schedules( + origin: OriginFor, + asset: AssetIdOf, + schedule1_index: u32, + schedule2_index: u32, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + if schedule1_index == schedule2_index { + return Ok(()) + }; + let schedule1_index = schedule1_index as usize; + let schedule2_index = schedule2_index as usize; + + let schedules = Vesting::::get(&asset, &who).ok_or(Error::::NotVesting)?; + let merge_action = + VestingAction::Merge { index1: schedule1_index, index2: schedule2_index }; + + let (schedules, locked_now) = Self::exec_action(schedules.to_vec(), merge_action)?; + + Self::write_vesting(asset.clone(), &who, schedules)?; + Self::write_lock(asset, &who, locked_now)?; + + Ok(()) + } + + /// Force remove a vesting schedule + /// + /// The dispatch origin for this call must be of `ForceOrigin`. + /// + /// - `asset`: Id of the asset class of the asset account for which the vesting applies. + /// - `target`: An account that has a vesting schedule + /// - `schedule_index`: The vesting schedule index that should be removed + #[pallet::call_index(5)] + #[pallet::weight(T::WeightInfo::force_remove_vesting_schedule(T::MAX_VESTING_SCHEDULES))] + pub fn force_remove_vesting_schedule( + origin: OriginFor, + asset: AssetIdOf, + target: ::Source, + schedule_index: u32, + ) -> DispatchResultWithPostInfo { + ensure_root(origin)?; + let who = T::Lookup::lookup(target)?; + + let schedules_count = + Vesting::::decode_len(asset.clone(), &who).unwrap_or_default(); + ensure!(schedule_index < schedules_count as u32, Error::::InvalidScheduleParams); + + Self::remove_vesting_schedule(asset, &who, schedule_index)?; + + Ok(Some(T::WeightInfo::force_remove_vesting_schedule(schedules_count as u32)).into()) + } + } +} + +impl, I: 'static> Pallet { + /// Public function for accessing vesting storage + pub fn vesting( + asset: AssetIdOf, + account: T::AccountId, + ) -> Option< + BoundedVec, BlockNumberFor>, MaxVestingSchedulesGet>, + > { + Vesting::::get(asset, 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>> { + 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); + + // Check if one or both schedules have ended. + match (schedule1_ending_block <= now_as_balance, schedule2_ending_block <= now_as_balance) { + // If both schedules have ended, we don't merge and exit early. + (true, true) => return None, + // If one schedule has ended, we treat the one that has not ended as the new + // merged schedule. + (true, false) => return Some(schedule2), + (false, true) => return Some(schedule1), + // If neither schedule has ended don't exit early. + _ => {}, + } + + let frozen = schedule1 + .locked_at::(now) + .saturating_add(schedule2.locked_at::(now)); + // This shouldn't happen because we know at least one ending block is greater than now, + // thus at least a schedule a some locked balance. + debug_assert!( + !frozen.is_zero(), + "merge_vesting_info validation checks failed to catch a locked of 0" + ); + + let ending_block = schedule1_ending_block.max(schedule2_ending_block); + let starting_block = now.max(schedule1.starting_block()).max(schedule2.starting_block()); + + let per_block = { + let duration = ending_block + .saturating_sub(T::BlockNumberToBalance::convert(starting_block)) + .max(One::one()); + (frozen / duration).max(One::one()) + }; + + let schedule = VestingInfo::new(frozen, per_block, starting_block); + debug_assert!(schedule.is_valid(), "merge_vesting_info schedule validation check failed"); + + Some(schedule) + } + + // Execute a vested transfer from `source` to `target` with the given `schedule`. + fn do_vested_transfer( + asset: AssetIdOf, + source: &T::AccountId, + target: &T::AccountId, + schedule: VestingInfo, BlockNumberFor>, + ) -> DispatchResult { + // Validate user inputs. + ensure!(schedule.locked() >= T::MinVestedTransfer::get(), Error::::AmountLow); + if !schedule.is_valid() { + return Err(Error::::InvalidScheduleParams.into()) + }; + + // Check we can add to this account prior to any storage writes. + Self::can_add_vesting_schedule( + asset.clone(), + target, + schedule.locked(), + schedule.per_block(), + schedule.starting_block(), + )?; + + T::Assets::transfer( + asset.clone(), + source, + target, + schedule.locked(), + Preservation::Expendable, + )?; + + // We can't let this fail because the currency transfer has already happened. + // Must be successful as it has been checked before. + // Better to return error on failure anyway. + let res = Self::add_vesting_schedule( + asset, + target, + schedule.locked(), + schedule.per_block(), + schedule.starting_block(), + ); + debug_assert!(res.is_ok(), "Failed to add a schedule when we had to succeed."); + + Ok(()) + } + + /// Iterate through the schedules to track the current locked amount and + /// filter out completed and specified schedules. + /// + /// Returns a tuple that consists of: + /// - Vec of vesting schedules, where completed schedules and those specified + /// by filter are removed. (Note the vec is not checked for respecting + /// bounded length.) + /// - The amount locked at the current block number based on the given schedules. + /// + /// NOTE: the amount locked does not include any schedules that are filtered out via `action`. + fn report_schedule_updates( + schedules: Vec, BlockNumberFor>>, + action: VestingAction, + ) -> (Vec, BlockNumberFor>>, BalanceOf) { + let now = T::BlockNumberProvider::current_block_number(); + + let mut total_locked_now: BalanceOf = Zero::zero(); + let filtered_schedules = action + .pick_schedules::(schedules) + .filter(|schedule| { + let locked_now = schedule.locked_at::(now); + let keep = !locked_now.is_zero(); + if keep { + total_locked_now = total_locked_now.saturating_add(locked_now); + } + keep + }) + .collect::>(); + + (filtered_schedules, total_locked_now) + } + + /// Write an accounts updated vesting lock to storage. + fn write_lock( + asset: AssetIdOf, + who: &T::AccountId, + total_locked_now: BalanceOf, + ) -> DispatchResult { + T::Freezer::set_freeze( + asset.clone(), + &FreezeReason::::Vesting.into(), + &who, + total_locked_now, + )?; + + if total_locked_now.is_zero() { + Self::deposit_event(Event::::VestingCompleted { asset, account: who.clone() }); + } else { + Self::deposit_event(Event::::VestingUpdated { + asset, + account: who.clone(), + unvested: total_locked_now, + }); + } + + Ok(()) + } + + /// Write an accounts updated vesting schedules to storage. + fn write_vesting( + asset: AssetIdOf, + who: &T::AccountId, + schedules: Vec, BlockNumberFor>>, + ) -> Result<(), DispatchError> { + let schedules: BoundedVec< + VestingInfo, BlockNumberFor>, + MaxVestingSchedulesGet, + > = schedules.try_into().map_err(|_| Error::::AtMaxVestingSchedules)?; + + if schedules.len() == 0 { + Vesting::::remove(asset, &who); + } else { + Vesting::::insert(asset, who, schedules) + } + + Ok(()) + } + + /// Unlock any vested funds of `who`. + fn do_vest(asset: AssetIdOf, who: T::AccountId) -> DispatchResult { + let schedules = Vesting::::get(&asset, &who).ok_or(Error::::NotVesting)?; + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(asset.clone(), &who, schedules)?; + Self::write_lock(asset, &who, locked_now)?; + + Ok(()) + } + + /// Execute a `VestingAction` against the given `schedules`. Returns the updated schedules + /// and locked amount. + fn exec_action( + schedules: Vec, BlockNumberFor>>, + action: VestingAction, + ) -> 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)?; + + // 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 + // (assuming initial state was valid). + let (mut schedules, mut locked_now) = + Self::report_schedule_updates(schedules.to_vec(), action); + + let now = T::BlockNumberProvider::current_block_number(); + if let Some(new_schedule) = Self::merge_vesting_info(now, schedule1, schedule2) { + // Merging created a new schedule so we: + // 1) need to add it to the accounts vesting schedule collection, + schedules.push(new_schedule); + // (we use `locked_at` in case this is a schedule that started in the past) + let new_schedule_locked = + new_schedule.locked_at::(now); + // and 2) update the locked amount to reflect the schedule we just added. + locked_now = locked_now.saturating_add(new_schedule_locked); + } // In the None case there was no new schedule to account for. + + (schedules, locked_now) + }, + _ => Self::report_schedule_updates(schedules.to_vec(), action), + }; + + debug_assert!( + locked_now > Zero::zero() && schedules.len() > 0 || + locked_now == Zero::zero() && schedules.len() == 0 + ); + + Ok((schedules, locked_now)) + } +} + +impl, I: 'static> VestingSchedule for Pallet +where + BalanceOf: MaybeSerializeDeserialize + Debug, +{ + type Moment = BlockNumberFor; + type AssetId = AssetIdOf; + type Balance = BalanceOf; + + fn vesting_balance(asset: AssetIdOf, who: &T::AccountId) -> Option> { + Vesting::::get(&asset, who).map(|v| { + 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) + }); + total_locked_now + }) + } + + fn add_vesting_schedule( + asset: AssetIdOf, + who: &T::AccountId, + locked: BalanceOf, + per_block: BalanceOf, + starting_block: BlockNumberFor, + ) -> DispatchResult { + if locked.is_zero() { + return Ok(()) + } + + 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()) + }; + + let mut schedules = Vesting::::get(&asset, 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); + + let (schedules, locked_now) = + Self::exec_action(schedules.to_vec(), VestingAction::Passive)?; + + Self::write_vesting(asset.clone(), who, schedules)?; + Self::write_lock(asset, who, locked_now)?; + + Ok(()) + } + + fn can_add_vesting_schedule( + asset: AssetIdOf, + who: &T::AccountId, + 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()) + } + + ensure!( + (Vesting::::decode_len(asset, who).unwrap_or_default() as u32) < + T::MAX_VESTING_SCHEDULES, + Error::::AtMaxVestingSchedules + ); + + Ok(()) + } + + fn remove_vesting_schedule( + asset: AssetIdOf, + who: &T::AccountId, + schedule_index: u32, + ) -> DispatchResult { + let schedules = Vesting::::get(&asset, 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)?; + + Self::write_vesting(asset.clone(), who, schedules)?; + Self::write_lock(asset, who, locked_now)?; + Ok(()) + } +} + +impl, I: 'static> VestedTransfer for Pallet +where + BalanceOf: MaybeSerializeDeserialize + Debug, +{ + type Moment = BlockNumberFor; + type AssetId = AssetIdOf; + type Balance = BalanceOf; + + fn vested_transfer( + asset: AssetIdOf, + source: &T::AccountId, + target: &T::AccountId, + locked: BalanceOf, + per_block: BalanceOf, + starting_block: BlockNumberFor, + ) -> DispatchResult { + use storage::{with_transaction, TransactionOutcome}; + let schedule = VestingInfo::new(locked, per_block, starting_block); + with_transaction(|| -> TransactionOutcome { + let result = Self::do_vested_transfer(asset, source, target, schedule); + + match &result { + Ok(()) => TransactionOutcome::Commit(result), + _ => TransactionOutcome::Rollback(result), + } + }) + } +} diff --git a/substrate/frame/assets-vesting/src/mock.rs b/substrate/frame/assets-vesting/src/mock.rs new file mode 100644 index 0000000000000..52edced9bbc23 --- /dev/null +++ b/substrate/frame/assets-vesting/src/mock.rs @@ -0,0 +1,228 @@ +// This file is part of Substrate. + +// 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. + +use crate as pallet_assets_vesting; +pub use frame::{ + deps::{codec::Encode, sp_runtime::traits::Identity}, + testing_prelude::{Get, *}, +}; +use std::collections::HashSet; + +#[frame_construct_runtime] +mod runtime { + // The main runtime + #[runtime::runtime] + // Runtime Types to be generated + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeFreezeReason, + RuntimeHoldReason, + RuntimeSlashReason, + RuntimeLockId, + RuntimeTask, + RuntimeViewFunction + )] + pub struct Test; + + #[runtime::pallet_index(0)] + pub type System = frame_system; + #[runtime::pallet_index(1)] + pub type Balances = pallet_balances; + #[runtime::pallet_index(2)] + pub type Assets = pallet_assets; + #[runtime::pallet_index(3)] + pub type AssetsFreezer = pallet_assets_freezer; + #[runtime::pallet_index(4)] + pub type AssetsVesting = pallet_assets_vesting; +} + +type Block = MockBlock; +pub type BlockNumber = BlockNumberFor; +pub type AccountId = ::AccountId; +type ExistentialDeposit = ::ExistentialDeposit; +pub type Balance = ::Balance; +pub type AssetId = ::AssetId; + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type AccountData = pallet_balances::AccountData; + type Block = Block; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; + type MaxFreezes = ConstU32<2>; +} + +#[derive_impl(pallet_assets::config_preludes::TestDefaultConfig)] +impl pallet_assets::Config for Test { + type Currency = Balances; + type ForceOrigin = EnsureRoot; + type CreateOrigin = EnsureSigned; + type Freezer = AssetsFreezer; +} + +impl pallet_assets_freezer::Config for Test { + type RuntimeFreezeReason = RuntimeFreezeReason; + type RuntimeEvent = RuntimeEvent; +} + +parameter_types! { + pub const MinVestedTransfer: u64 = 256 * 2; +} + +impl pallet_assets_vesting::Config for Test { + type RuntimeEvent = RuntimeEvent; + type ForceOrigin = EnsureRoot; + type Assets = Assets; + type Freezer = AssetsFreezer; + type BlockNumberToBalance = Identity; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); + type MinVestedTransfer = MinVestedTransfer; + type BlockNumberProvider = System; + const MAX_VESTING_SCHEDULES: u32 = 3; +} + +// Test Externalities + +#[derive(Clone)] +pub(crate) struct AssetsGenesis { + id: AssetId, + minimum_balance: Balance, + owner: AccountId, + accounts: Vec<(AccountId, Balance)>, +} + +pub struct ExtBuilder { + assets: Option>, + vesting_genesis_config: Option>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + Self { assets: None, vesting_genesis_config: None } + } +} + +impl ExtBuilder { + pub fn with_min_balance(self, id: AssetId, minimum_balance: Balance) -> Self { + self.with_asset( + id, + 0, + minimum_balance, + vec![(1, 10), (2, 20), (3, 30), (4, 40), (12, 10), (13, 9999)] + .iter() + .map(|(who, amount)| (*who, *amount * minimum_balance)) + .collect(), + ) + .with_vesting_genesis_config((id, 1, 0, 10, 5 * minimum_balance)) + .with_vesting_genesis_config((id, 2, 10, 20, 0)) + .with_vesting_genesis_config((id, 12, 10, 20, 5 * minimum_balance)) + } + + pub fn with_asset( + mut self, + id: AssetId, + owner: AccountId, + minimum_balance: Balance, + accounts: Vec<(AccountId, Balance)>, + ) -> Self { + let mut assets = self.assets.unwrap_or(vec![]); + assets.push(AssetsGenesis { id, owner, minimum_balance, accounts }); + self.assets = Some(assets); + self + } + + pub fn with_vesting_genesis_config( + mut self, + config: (AssetId, AccountId, BlockNumber, BlockNumber, Balance), + ) -> Self { + let mut vesting_genesis_config = self.vesting_genesis_config.unwrap_or(vec![]); + vesting_genesis_config.push(config); + self.vesting_genesis_config = Some(vesting_genesis_config); + self + } + + pub fn build(self) -> TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + let assets = self.assets.unwrap_or(vec![AssetsGenesis { + id: 1, + minimum_balance: 1, + owner: 0, + accounts: vec![(1, 10), (2, 20), (3, 30), (4, 40), (12, 10), (13, 9999)], + }]); + + // Configure genesis for `Balances` + let balances: HashSet<(AccountId, Balance)> = assets + .clone() + .into_iter() + .flat_map(|AssetsGenesis { accounts, .. }| { + accounts + .iter() + .map(|(who, _)| (*who, >::get())) + .collect::>() + }) + .collect(); + pallet_balances::GenesisConfig:: { + balances: balances.into_iter().collect(), + dev_accounts: None, + } + .assimilate_storage(&mut t) + .unwrap(); + + // Configure genesis for `Assets` + let mut assets_genesis = pallet_assets::GenesisConfig:: { + assets: vec![], + accounts: vec![], + metadata: vec![], + next_asset_id: None, + }; + for AssetsGenesis { id, owner, minimum_balance, accounts } in assets.clone() { + assets_genesis.assets.push((id, owner, true, minimum_balance)); + assets_genesis + .accounts + .append(&mut accounts.into_iter().map(|(who, amount)| (id, who, amount)).collect()); + } + assets_genesis.assimilate_storage(&mut t).unwrap(); + + // Configure genesis for `AssetsVesting` + pallet_assets_vesting::GenesisConfig:: { + vesting: self + .vesting_genesis_config + .unwrap_or(vec![ + (assets[0].id, 1, 0, 10, 5 * assets[0].minimum_balance), + (assets[0].id, 2, 10, 20, 0), + (assets[0].id, 12, 10, 20, 5 * assets[0].minimum_balance), + ]) + .into_iter() + .map(|(id, who, begin, length, liquid)| (id.encode(), who, begin, length, liquid)) + .collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut ext = TestExternalities::new(t); + ext.execute_with(|| System::set_block_number(1)); + ext + } +} diff --git a/substrate/frame/assets-vesting/src/tests.rs b/substrate/frame/assets-vesting/src/tests.rs new file mode 100644 index 0000000000000..f44a599f76971 --- /dev/null +++ b/substrate/frame/assets-vesting/src/tests.rs @@ -0,0 +1,1620 @@ +// This file is part of Substrate. + +// 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. + +use super::{mock::*, AccountIdOf, AssetIdOf, Error, Vesting as VestingStorage, VestingInfo}; +use crate::mock::frame_system::RawOrigin; +use codec::EncodeLike; +use frame::traits::fungibles::VestingSchedule; + +const ASSET_ID: AssetId = 1; +const MINIMUM_BALANCE: Balance = 256; +const MIN_VESTED_TRANSFER: Balance = ::MinVestedTransfer::get(); +const MAX_VESTING_SCHEDULES: u32 = ::MAX_VESTING_SCHEDULES; + +/// Calls vest, and asserts that there is no entry for `account` +/// in the `Vesting` storage item. +fn vest_and_assert_no_vesting(asset: AssetId, account: AccountId) +where + AssetId: EncodeLike>, + AccountId: EncodeLike>, + T: crate::Config + pallet_assets::Config, +{ + // Its ok for this to fail because the user may already have no schedules. + let _result = AssetsVesting::vest(Some(account).into(), asset); + assert!(!>::contains_key(asset, account)); +} + +mod vesting_status { + use super::*; + + #[test] + fn check_vesting_status() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let user_1 = 1; + let user_2 = 2; + let user_12 = 12; + + let user1_account_balance = Assets::balance(ASSET_ID, &user_1); + let user2_account_balance = Assets::balance(ASSET_ID, &user_2); + let user12_account_balance = Assets::balance(ASSET_ID, &user_12); + + assert_eq!(user1_account_balance, MINIMUM_BALANCE * 10); // Account 1 has balance + assert_eq!(user2_account_balance, MINIMUM_BALANCE * 20); // Account 2 has balance + assert_eq!(user12_account_balance, MINIMUM_BALANCE * 10); // Account 12 has balance + + let user1_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + 128, // Vesting over 10 blocks + 0, + ); + let user2_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vesting over 20 blocks + 10, + ); + let user12_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + 64, // Vesting over 20 blocks + 10, + ); + + assert_eq!( + VestingStorage::::get(ASSET_ID, &user_1).unwrap(), + vec![user1_vesting_schedule] + ); // Account 1 has a vesting schedule + assert_eq!( + VestingStorage::::get(ASSET_ID, &user_2).unwrap(), + vec![user2_vesting_schedule] + ); // Account 2 has a vesting schedule + assert_eq!( + VestingStorage::::get(ASSET_ID, &user_12).unwrap(), + vec![user12_vesting_schedule] + ); // Account 12 has a vesting schedule + + // Account 1 has only 128 units vested from their illiquid MINIMUM_BALANCE * 5 units + // at block 1 + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &user_1), Some(128 * 9)); + // Account 2 has their full balance locked + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &user_2), + Some(user2_account_balance) + ); + // Account 12 has only their illiquid funds locked + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &user_12), + Some(user12_account_balance - MINIMUM_BALANCE * 5) + ); + + System::set_block_number(10); + assert_eq!(System::block_number(), 10); + + // Account 1 has fully vested by block 10 + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(0)); + // Account 2 has started vesting by block 10 + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &2), + Some(user2_account_balance) + ); + // Account 12 has started vesting by block 10 + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &12), + Some(user12_account_balance - MINIMUM_BALANCE * 5) + ); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(0)); // Account 1 is still fully vested, and not negative + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &2), Some(0)); // Account 2 has fully vested by block 30 + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &12), Some(0)); // Account 2 has fully vested by block 30 + + // Once we unlock the funds, they are removed from storage. + vest_and_assert_no_vesting::(ASSET_ID, 1); + vest_and_assert_no_vesting::(ASSET_ID, 2); + vest_and_assert_no_vesting::(ASSET_ID, 12); + }); + } + + #[test] + fn check_vesting_status_for_multi_schedule_account() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + assert_eq!(System::block_number(), 1); + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vesting over 20 blocks + 10, + ); + // Account 2 already has a vesting schedule. + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + // Account 2's free balance is from sched0. + let account_balance = Assets::balance(ASSET_ID, &2); + assert_eq!(account_balance, MINIMUM_BALANCE * (20)); + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &2), Some(account_balance)); + + // Add a 2nd schedule that is already unlocking by block #1. + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 10, + MINIMUM_BALANCE, // Vesting over 10 blocks + 0, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 2, sched1)); + // Free balance is equal to the two existing schedules total amount. + let account_balance = Assets::balance(ASSET_ID, &2); + assert_eq!(account_balance, MINIMUM_BALANCE * (10 + 20)); + // The most recently added schedule exists. + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1] + ); + // sched1 has free funds at block #1, but nothing else. + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &2), + Some(account_balance - sched1.per_block()) + ); + + // Add a 3rd schedule. + let sched2 = VestingInfo::new( + MINIMUM_BALANCE * 30, + MINIMUM_BALANCE, // Vesting over 30 blocks + 5, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 2, sched2)); + + System::set_block_number(9); + // Free balance is equal to the 3 existing schedules total amount. + let account_balance = Assets::balance(ASSET_ID, &2); + assert_eq!(account_balance, MINIMUM_BALANCE * (10 + 20 + 30)); + // sched1 and sched2 are freeing funds at block #9. + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &2), + Some(account_balance - sched1.per_block() * 9 - sched2.per_block() * 4) + ); + + System::set_block_number(20); + // At block #20 sched1 is fully unlocked while sched2 and sched0 are partially + // unlocked. + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &2), + Some( + account_balance - + sched1.locked() - sched2.per_block() * 15 - + sched0.per_block() * 10 + ) + ); + + System::set_block_number(30); + // At block #30 sched0 and sched1 are fully unlocked while sched2 is partially + // unlocked. + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &2), + Some( + account_balance - + sched1.locked() - sched2.per_block() * 25 - + sched0.locked() + ) + ); + + // At block #35 sched2 fully unlocks and thus all schedules funds are unlocked. + System::set_block_number(35); + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &2), Some(0)); + // Since we have not called any extrinsics that would unlock funds the schedules + // are still in storage, + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1, sched2] + ); + // but once we unlock the funds, they are removed from storage. + vest_and_assert_no_vesting::(ASSET_ID, 2); + }); + } + + #[test] + fn unvested_balance_should_not_transfer() { + ExtBuilder::default().with_min_balance(ASSET_ID, 10).build().execute_with(|| { + let user1_account_balance = Assets::balance(ASSET_ID, &1); + assert_eq!(user1_account_balance, 100); // Account 1 has account balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(45)); + // Account 1 cannot send more than vested amount... + // TODO: this error should change to `TokenError::Frozen` once #4530 is merged + assert_noop!( + Assets::transfer(Some(1).into(), ASSET_ID, 2, 56), + pallet_assets::Error::::BalanceLow + ); + }); + } +} + +mod vest { + use super::*; + + #[test] + fn vested_balance_should_transfer() { + ExtBuilder::default().with_min_balance(ASSET_ID, 10).build().execute_with(|| { + let user1_account_balance = Assets::balance(ASSET_ID, &1); + assert_eq!(user1_account_balance, 100); // Account 1 has account balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(45)); + assert_ok!(AssetsVesting::vest(Some(1).into(), ASSET_ID)); + // TODO: this value should be changed to 55 once #4530 is merged + assert_ok!(Assets::transfer(Some(1).into(), ASSET_ID, 2, 45)); + }); + } + + #[test] + fn vested_balance_should_transfer_with_multi_sched() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new(5 * MINIMUM_BALANCE, 128, 0); + assert_ok!(AssetsVesting::vested_transfer(Some(13).into(), ASSET_ID, 1, sched0)); + // Total 10*ED locked for all the schedules. + assert_eq!( + VestingStorage::::get(ASSET_ID, &1).unwrap(), + vec![sched0, sched0] + ); + + let user1_account_balance = Assets::balance(ASSET_ID, &1); + assert_eq!(user1_account_balance, 3840); // Account 1 has account balance + + // Account 1 has only 256 units unlocking at block 1 (plus 1280 already fee). + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(2304)); + assert_ok!(AssetsVesting::vest(Some(1).into(), ASSET_ID)); + // TODO: this value should be changed to 1536 once #4530 is merged + assert_ok!(Assets::transfer(Some(1).into(), ASSET_ID, 2, 1280)); + }); + } + + #[test] + fn non_vested_cannot_vest() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + assert!(!>::contains_key(ASSET_ID, 4)); + assert_noop!( + AssetsVesting::vest(Some(4).into(), ASSET_ID), + Error::::NotVesting + ); + }); + } +} + +mod vest_other { + use super::*; + + #[test] + fn vested_balance_should_transfer_using_vest_other() { + ExtBuilder::default().with_min_balance(ASSET_ID, 10).build().execute_with(|| { + let user1_account_balance = Assets::balance(ASSET_ID, &1); + assert_eq!(user1_account_balance, 100); // Account 1 has account balance + // Account 1 has only 5 units vested at block 1 (plus 50 unvested) + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(45)); + assert_ok!(AssetsVesting::vest_other(Some(2).into(), ASSET_ID, 1)); + // TODO: this value should be changed to 55 once #4530 is merged + assert_ok!(Assets::transfer(Some(1).into(), ASSET_ID, 2, 45)); + }); + } + + #[test] + fn vested_balance_should_transfer_using_vest_other_with_multi_sched() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new(5 * MINIMUM_BALANCE, 128, 0); + assert_ok!(AssetsVesting::vested_transfer(Some(13).into(), ASSET_ID, 1, sched0)); + // Total of 10*ED of locked for all the schedules. + assert_eq!( + VestingStorage::::get(ASSET_ID, &1).unwrap(), + vec![sched0, sched0] + ); + + let user1_account_balance = Assets::balance(ASSET_ID, &1); + assert_eq!(user1_account_balance, 3840); // Account 1 has account balance + + // Account 1 has only 256 units unlocking at block 1 (plus 1280 already free). + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(2304)); + assert_ok!(AssetsVesting::vest_other(Some(2).into(), ASSET_ID, 1)); + // TODO: this value should be changed to 1536 once #4530 is merged + assert_ok!(Assets::transfer(Some(1).into(), ASSET_ID, 2, 1280)); + }); + } + + #[test] + fn non_vested_cannot_vest_other() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + assert!(!>::contains_key(ASSET_ID, 4)); + assert_noop!( + AssetsVesting::vest_other(Some(3).into(), ASSET_ID, 4), + Error::::NotVesting + ); + }); + } +} + +mod transfer_with_vested_balance { + use super::*; + + #[test] + fn extra_balance_should_transfer() { + ExtBuilder::default().with_min_balance(ASSET_ID, 10).build().execute_with(|| { + assert_ok!(Assets::transfer(Some(3).into(), ASSET_ID, 1, 100)); + assert_ok!(Assets::transfer(Some(3).into(), ASSET_ID, 2, 100)); + + let user1_account_balance = Assets::balance(ASSET_ID, &1); + assert_eq!(user1_account_balance, 200); // Account 1 has 100 more free balance than normal + + let user2_account_balance = Assets::balance(ASSET_ID, &2); + assert_eq!(user2_account_balance, 300); // Account 2 has 100 more free balance than normal + + // Account 1 has only 5 units vested at block 1 (plus 150 unvested) + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &1), Some(45)); + assert_ok!(AssetsVesting::vest(Some(1).into(), ASSET_ID)); + // Account 1 can send extra units gained + // TODO: this value should be changed to 155 once #4530 is merged + assert_ok!(Assets::transfer(Some(1).into(), ASSET_ID, 3, 145)); + + // Account 2 has no units vested at block 1, but gained 100 + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &2), Some(200)); + assert_ok!(AssetsVesting::vest(Some(2).into(), ASSET_ID)); + // Account 2 can send extra units gained + // TODO: this value should be changed to 100 once #4530 is merged + assert_ok!(Assets::transfer(Some(2).into(), ASSET_ID, 3, 90)); + }); + } + + #[test] + fn liquid_funds_should_transfer_with_delayed_vesting() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let user12_account_balance = Assets::balance(ASSET_ID, &12); + + // Account 12 has free balance + assert_eq!(user12_account_balance, MINIMUM_BALANCE * 10); + // Account 12 has liquid funds + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &12), + Some(user12_account_balance - MINIMUM_BALANCE * 5) + ); + + // Account 12 has delayed vesting + let user12_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + // Vesting over 20 blocks + 64, + 10, + ); + assert_eq!( + VestingStorage::::get(ASSET_ID, &12).unwrap(), + vec![user12_vesting_schedule] + ); + + // Account 12 can still send liquid funds + // TODO: this value should be changed to MINIMUM_BALANCE * 5 once #4530 is merged + assert_ok!(Assets::transfer(Some(12).into(), ASSET_ID, 3, MINIMUM_BALANCE * 4)); + }); + } +} + +mod vested_transfer { + use super::*; + + #[test] + fn vested_transfer_works() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let user3_account_balance = Assets::balance(ASSET_ID, &3); + let user4_account_balance = Assets::balance(ASSET_ID, &4); + assert_eq!(user3_account_balance, MINIMUM_BALANCE * 30); + assert_eq!(user4_account_balance, MINIMUM_BALANCE * 40); + // Account 4 should not have any vesting yet. + assert_eq!(VestingStorage::::get(ASSET_ID, &4), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + 64, // Vesting over 20 blocks + 10, + ); + assert_ok!(AssetsVesting::vested_transfer( + Some(3).into(), + ASSET_ID, + 4, + new_vesting_schedule + )); + // Now account 4 should have vesting. + assert_eq!( + VestingStorage::::get(ASSET_ID, &4).unwrap(), + vec![new_vesting_schedule] + ); + // Ensure the transfer happened correctly. + let user3_account_balance_updated = Assets::balance(ASSET_ID, &3); + assert_eq!(user3_account_balance_updated, MINIMUM_BALANCE * 25); + let user4_account_balance_updated = Assets::balance(ASSET_ID, &4); + assert_eq!(user4_account_balance_updated, MINIMUM_BALANCE * 45); + // Account 4 has 5 * 256 locked. + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(MINIMUM_BALANCE * 5)); + + System::set_block_number(20); + assert_eq!(System::block_number(), 20); + + // Account 4 has 5 * 64 units vested by block 20. + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(10 * 64)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + // Account 4 has fully vested, + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(ASSET_ID, 4); + }); + } + + #[test] + fn vested_transfer_correctly_fails() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let user2_account_balance = Assets::balance(ASSET_ID, &2); + let user4_account_balance = Assets::balance(ASSET_ID, &4); + assert_eq!(user2_account_balance, MINIMUM_BALANCE * 20); + assert_eq!(user4_account_balance, MINIMUM_BALANCE * 40); + + // Account 2 should already have a vesting schedule. + let user2_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vesting over 20 blocks + 10, + ); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![user2_vesting_schedule] + ); + + // Fails due to too low transfer amount. + let new_vesting_schedule_too_low = + VestingInfo::new(MIN_VESTED_TRANSFER - 1, 64, 10); + assert_noop!( + AssetsVesting::vested_transfer( + Some(3).into(), + ASSET_ID, + 4, + new_vesting_schedule_too_low + ), + Error::::AmountLow, + ); + + // `per_block` is 0, which would result in a schedule with infinite duration. + let schedule_per_block_0 = VestingInfo::new(MIN_VESTED_TRANSFER, 0, 10); + assert_noop!( + AssetsVesting::vested_transfer( + Some(13).into(), + ASSET_ID, + 4, + schedule_per_block_0 + ), + Error::::InvalidScheduleParams, + ); + + // `locked` is 0. + let schedule_locked_0 = VestingInfo::new(0, 1, 10); + assert_noop!( + AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 4, schedule_locked_0), + Error::::AmountLow, + ); + + // Free balance has not changed. + assert_eq!(user2_account_balance, Assets::balance(ASSET_ID, &2)); + assert_eq!(user4_account_balance, Assets::balance(ASSET_ID, &4)); + // Account 4 has no schedules. + vest_and_assert_no_vesting::(ASSET_ID, 4); + }); + } + + #[test] + fn vested_transfer_allows_max_schedules() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let mut user_4_account_balance = Assets::balance(ASSET_ID, &4); + let max_schedules = MAX_VESTING_SCHEDULES; + let sched = VestingInfo::new( + MIN_VESTED_TRANSFER, + 1, // Vest over 2 * 256 blocks. + 10, + ); + + // Add max amount schedules to user 4. + for _ in 0..max_schedules { + assert_ok!(AssetsVesting::vested_transfer(Some(13).into(), ASSET_ID, 4, sched)); + } + + // The schedules count towards vesting balance + let transferred_amount = MIN_VESTED_TRANSFER * max_schedules as u64; + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(transferred_amount)); + // and free balance. + user_4_account_balance += transferred_amount; + assert_eq!(Assets::balance(ASSET_ID, &4), user_4_account_balance); + + // Cannot insert a 4th vesting schedule when `MaxVestingSchedules` === 3, + assert_noop!( + AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 4, sched), + Error::::AtMaxVestingSchedules, + ); + // so the free balance does not change. + assert_eq!(Assets::balance(ASSET_ID, &4), user_4_account_balance); + + // Account 4 has fully vested when all the schedules end, + System::set_block_number(MIN_VESTED_TRANSFER + sched.starting_block()); + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(ASSET_ID, 4); + }); + } +} + +mod force_vested_transfer { + use super::*; + + #[test] + fn force_vested_transfer_works() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let user3_account_balance = Assets::balance(ASSET_ID, &3); + let user4_account_balance = Assets::balance(ASSET_ID, &4); + assert_eq!(user3_account_balance, MINIMUM_BALANCE * 30); + assert_eq!(user4_account_balance, MINIMUM_BALANCE * 40); + // Account 4 should not have any vesting yet. + assert_eq!(VestingStorage::::get(ASSET_ID, &4), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + 64, // Vesting over 20 blocks + 10, + ); + + assert_noop!( + AssetsVesting::force_vested_transfer( + Some(4).into(), + ASSET_ID, + 3, + 4, + new_vesting_schedule + ), + BadOrigin + ); + assert_ok!(AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 3, + 4, + new_vesting_schedule + )); + // Now account 4 should have vesting. + assert_eq!( + VestingStorage::::get(ASSET_ID, &4).unwrap()[0], + new_vesting_schedule + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &4).unwrap().len(), 1); + // Ensure the transfer happened correctly. + let user3_account_balance_updated = Assets::balance(ASSET_ID, &3); + assert_eq!(user3_account_balance_updated, MINIMUM_BALANCE * 25); + let user4_account_balance_updated = Assets::balance(ASSET_ID, &4); + assert_eq!(user4_account_balance_updated, MINIMUM_BALANCE * 45); + // Account 4 has 5 * ED locked. + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(MINIMUM_BALANCE * 5)); + + System::set_block_number(20); + assert_eq!(System::block_number(), 20); + + // Account 4 has 5 * 64 units vested by block 20. + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(10 * 64)); + + System::set_block_number(30); + assert_eq!(System::block_number(), 30); + + // Account 4 has fully vested, + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(ASSET_ID, 4); + }); + } + + #[test] + fn force_vested_transfer_correctly_fails() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let user2_account_balance = Assets::balance(ASSET_ID, &2); + let user4_account_balance = Assets::balance(ASSET_ID, &4); + assert_eq!(user2_account_balance, MINIMUM_BALANCE * 20); + assert_eq!(user4_account_balance, MINIMUM_BALANCE * 40); + // Account 2 should already have a vesting schedule. + let user2_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vesting over 20 blocks + 10, + ); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![user2_vesting_schedule] + ); + + // Too low transfer amount. + let new_vesting_schedule_too_low = + VestingInfo::new(MIN_VESTED_TRANSFER - 1, 64, 10); + assert_noop!( + AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 3, + 4, + new_vesting_schedule_too_low + ), + Error::::AmountLow, + ); + + // `per_block` is 0. + let schedule_per_block_0 = VestingInfo::new(MIN_VESTED_TRANSFER, 0, 10); + assert_noop!( + AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 13, + 4, + schedule_per_block_0 + ), + Error::::InvalidScheduleParams, + ); + + // `locked` is 0. + let schedule_locked_0 = VestingInfo::new(0, 1, 10); + assert_noop!( + AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 3, + 4, + schedule_locked_0 + ), + Error::::AmountLow, + ); + + // Verify no currency transfer happened. + assert_eq!(user2_account_balance, Assets::balance(ASSET_ID, &2)); + assert_eq!(user4_account_balance, Assets::balance(ASSET_ID, &4)); + // Account 4 has no schedules. + vest_and_assert_no_vesting::(ASSET_ID, 4); + }); + } + + #[test] + fn force_vested_transfer_allows_max_schedules() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let mut user_4_account_balance = Assets::balance(ASSET_ID, &4); + let max_schedules = MAX_VESTING_SCHEDULES; + let sched = VestingInfo::new( + MIN_VESTED_TRANSFER, + 1, // Vest over 2 * 256 blocks. + 10, + ); + + // Add max amount schedules to user 4. + for _ in 0..max_schedules { + assert_ok!(AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 13, + 4, + sched + )); + } + + // The schedules count towards vesting balance. + let transferred_amount = MIN_VESTED_TRANSFER * max_schedules as u64; + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(transferred_amount)); + // and free balance. + user_4_account_balance += transferred_amount; + assert_eq!(Assets::balance(ASSET_ID, &4), user_4_account_balance); + + // Cannot insert a 4th vesting schedule when `MaxVestingSchedules` === 3 + assert_noop!( + AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 3, + 4, + sched + ), + Error::::AtMaxVestingSchedules, + ); + // so the free balance does not change. + assert_eq!(Assets::balance(ASSET_ID, &4), user_4_account_balance); + + // Account 4 has fully vested when all the schedules end, + System::set_block_number(MIN_VESTED_TRANSFER + 10); + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(0)); + // and after unlocking its schedules are removed from storage. + vest_and_assert_no_vesting::(ASSET_ID, 4); + }); + } +} + +mod merge_schedules { + use super::*; + use frame::traits::{ + fungibles::Inspect, + tokens::{Fortitude::Polite, Preservation::Preserve}, + }; + + #[test] + fn merge_schedules_that_have_not_started() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vest over 20 blocks. + 10, + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + + // Add a schedule that is identical to the one that already exists. + assert_ok!(AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 2, sched0)); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched0] + ); + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + assert_ok!(AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1)); + + // Since we merged identical schedules, the new schedule finishes at the same + // time as the original, just with double the amount. + let sched1 = VestingInfo::new( + sched0.locked() * 2, + sched0.per_block() * 2, + 10, // Starts at the block the schedules are merged/ + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched1]); + + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + }); + } + + #[test] + fn merge_ongoing_schedules() { + // Merging two schedules that have started will vest both before merging. + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vest over 20 blocks. + 10, + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 10, + // Vest over 10 blocks. + MINIMUM_BALANCE, + // Start at block 15. + sched0.starting_block() + 5, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 2, sched1)); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1] + ); + + // Got to half way through the second schedule where both schedules are actively + // vesting. + let cur_block = 20; + System::set_block_number(cur_block); + + // Account 2 has no usable balances prior to the merge because they have not + // unlocked with `vest` yet. + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + + assert_ok!(AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1)); + + // Merging schedules un-vests all pre-existing schedules prior to merging, which is + // reflected in account 2's updated usable balance. + let sched0_vested_now = sched0.per_block() * (cur_block - sched0.starting_block()); + let sched1_vested_now = sched1.per_block() * (cur_block - sched1.starting_block()); + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + // TODO: this `- MINIMUM_BALANCE` should be removed once #4530 is merged. + sched0_vested_now + sched1_vested_now - MINIMUM_BALANCE + ); + + // The locked amount is the sum of what both schedules have locked at the current + // block. + let sched2_locked = sched1 + .locked_at::(cur_block) + .saturating_add(sched0.locked_at::(cur_block)); + // End block of the new schedule is the greater of either merged schedule. + let sched2_end = sched1 + .ending_block_as_balance::() + .max(sched0.ending_block_as_balance::()); + let sched2_duration = sched2_end - cur_block; + // Based off the new schedules total locked and its duration, we can calculate the + // amount to unlock per block. + let sched2_per_block = sched2_locked / sched2_duration; + + let sched2 = VestingInfo::new(sched2_locked, sched2_per_block, cur_block); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched2]); + + // And just to double check, we assert the new merged schedule we be cleaned up as + // expected. + System::set_block_number(30); + vest_and_assert_no_vesting::(ASSET_ID, 2); + }); + } + + #[test] + fn merging_shifts_other_schedules_index() { + // Schedules being merged are filtered out, schedules to the right of any merged + // schedule shift left and the merged schedule is always last. + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 10, + MINIMUM_BALANCE, // Vesting over 10 blocks. + 10, + ); + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 11, + MINIMUM_BALANCE, // Vesting over 11 blocks. + 11, + ); + let sched2 = VestingInfo::new( + MINIMUM_BALANCE * 12, + MINIMUM_BALANCE, // Vesting over 12 blocks. + 12, + ); + + // Account 3 starts out with no schedules, + assert_eq!(VestingStorage::::get(ASSET_ID, &3), None); + // and some usable balance. + let usable_balance = Assets::reducible_balance(ASSET_ID, &3, Preserve, Polite); + // TODO: this value should be changed to 30 * MINIMUM_BALANCE once #4530 is merged + assert_eq!(usable_balance, 29 * MINIMUM_BALANCE); + + let cur_block = 1; + assert_eq!(System::block_number(), cur_block); + + // Transfer the above 3 schedules to account 3. + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 3, sched0)); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 3, sched1)); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 3, sched2)); + + // With no schedules vested or merged they are in the order they are created + assert_eq!( + VestingStorage::::get(ASSET_ID, &3).unwrap(), + vec![sched0, sched1, sched2] + ); + // and the usable balance has not changed. + assert_eq!( + usable_balance, + Assets::reducible_balance(ASSET_ID, &3, Preserve, Polite) + ); + + assert_ok!(AssetsVesting::merge_schedules(Some(3).into(), ASSET_ID, 0, 2)); + + // Create the merged schedule of sched0 & sched2. + // The merged schedule will have the max possible starting block, + let sched3_start = sched1.starting_block().max(sched2.starting_block()); + // `locked` equal to the sum of the two schedules locked through the current block, + let sched3_locked = sched2.locked_at::(cur_block) + + sched0.locked_at::(cur_block); + // and will end at the max possible block. + let sched3_end = sched2 + .ending_block_as_balance::() + .max(sched0.ending_block_as_balance::()); + let sched3_duration = sched3_end - sched3_start; + let sched3_per_block = sched3_locked / sched3_duration; + let sched3 = VestingInfo::new(sched3_locked, sched3_per_block, sched3_start); + + // The not touched schedule moves left and the new merged schedule is appended. + assert_eq!( + VestingStorage::::get(ASSET_ID, &3).unwrap(), + vec![sched1, sched3] + ); + // The usable balance hasn't changed since none of the schedules have started. + assert_eq!( + Assets::reducible_balance(ASSET_ID, &3, Preserve, Polite), + usable_balance + ); + }); + } + + #[test] + fn merge_ongoing_and_yet_to_be_started_schedules() { + // Merge an ongoing schedule that has had `vest` called and a schedule that has not already + // started. + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vesting over 20 blocks + 10, + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + // Fast forward to half way through the life of sched1. + let mut cur_block = + (sched0.starting_block() + sched0.ending_block_as_balance::()) / 2; + assert_eq!(cur_block, 20); + System::set_block_number(cur_block); + + // Prior to vesting there is no usable balance. + let mut reducible_balance = 0; + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + reducible_balance + ); + // Vest the current schedules (which is just sched0 now). + AssetsVesting::vest(Some(2).into(), ASSET_ID).unwrap(); + + // After vesting the usable balance increases by the unlocked amount. + let sched0_vested_now = sched0.locked() - sched0.locked_at::(cur_block); + reducible_balance += sched0_vested_now; + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + // TODO: Remove the subtract of `MINIMUM_BALANCE` from this value after #4530 + // is merged. + reducible_balance - MINIMUM_BALANCE + ); + + // Go forward a block. + cur_block += 1; + System::set_block_number(cur_block); + + // And add a schedule that starts after this block, but before sched0 finishes. + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 10, + 1, // Vesting over 256 * 10 (2560) blocks + cur_block + 1, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 2, sched1)); + + // Merge the schedules before sched1 starts. + assert_ok!(AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1)); + // After merging, the usable balance only changes by the amount sched0 vested since + // we last called `vest` (which is just 1 block). The usable balance is not + // affected by sched1 because it has not started yet. + reducible_balance += sched0.per_block(); + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + // TODO: Remove the subtract of `MINIMUM_BALANCE` from this value after #4530 + // is merged. + reducible_balance - MINIMUM_BALANCE + ); + + // The resulting schedule will have the later starting block of the two, + let sched2_start = sched1.starting_block(); + // `locked` equal to the sum of the two schedules locked through the current block, + let sched2_locked = sched0.locked_at::(cur_block) + + sched1.locked_at::(cur_block); + // and will end at the max possible block. + let sched2_end = sched0 + .ending_block_as_balance::() + .max(sched1.ending_block_as_balance::()); + let sched2_duration = sched2_end - sched2_start; + let sched2_per_block = sched2_locked / sched2_duration; + + let sched2 = VestingInfo::new(sched2_locked, sched2_per_block, sched2_start); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched2]); + }); + } + + #[test] + fn merge_finished_and_ongoing_schedules() { + // If a schedule finishes by the current block we treat the ongoing schedule, + // without any alterations, as the merged one. + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // Vesting over 20 blocks. + 10, + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 40, + MINIMUM_BALANCE, // Vesting over 40 blocks. + 10, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(4).into(), ASSET_ID, 2, sched1)); + + // Transfer a 3rd schedule, so we can demonstrate how schedule indices change. + // (We are not merging this schedule.) + let sched2 = VestingInfo::new( + MINIMUM_BALANCE * 30, + MINIMUM_BALANCE, // Vesting over 30 blocks. + 10, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 2, sched2)); + + // The schedules are in expected order prior to merging. + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1, sched2] + ); + + // Fast forward to sched0's end block. + let cur_block = sched0.ending_block_as_balance::(); + System::set_block_number(cur_block); + assert_eq!(System::block_number(), 30); + + // Prior to `merge_schedules` and with no vest/vest_other called the user has no + // usable balance. + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + assert_ok!(AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1)); + + // sched2 is now the first, since sched0 & sched1 get filtered out while "merging". + // sched1 gets treated like the new merged schedule by getting pushed onto back + // of the vesting schedules vec. Note: sched0 finished at the current block. + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched2, sched1] + ); + + // sched0 has finished, so its funds are fully unlocked. + let sched0_unlocked_now = sched0.locked(); + // The remaining schedules are ongoing, so their funds are partially unlocked. + let sched1_unlocked_now = sched1.locked() - sched1.locked_at::(cur_block); + let sched2_unlocked_now = sched2.locked() - sched2.locked_at::(cur_block); + + // Since merging also vests all the schedules, the users usable balance after + // merging includes all pre-existing schedules unlocked through the current + // block, including schedules not merged. + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + // TODO: Remove the subtract of `MINIMUM_BALANCE` from this value after #4530 + // is merged. + sched0_unlocked_now + sched1_unlocked_now + sched2_unlocked_now - + MINIMUM_BALANCE, + ); + }); + } + + #[test] + fn merge_finishing_schedules_does_not_create_a_new_one() { + // If both schedules finish by the current block we don't create new one + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // 20 block duration. + 10, + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + // Create sched1 and transfer it to account 2. + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 30, + MINIMUM_BALANCE, // 30 block duration. + 10, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 2, sched1)); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1] + ); + + let all_scheds_end = sched0 + .ending_block_as_balance::() + .max(sched1.ending_block_as_balance::()); + + assert_eq!(all_scheds_end, 40); + System::set_block_number(all_scheds_end); + + // Prior to merge_schedules and with no vest/vest_other called the user has no + // usable balance. + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + + // Merge schedule 0 and 1. + assert_ok!(AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1)); + // The user no longer has any more vesting schedules because they both ended at the + // block they where merged, + assert!(!>::contains_key(ASSET_ID, &2)); + // and their usable balance has increased by the total amount locked in the merged + // schedules. + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + // TODO: Remove the subtract of `MINIMUM_BALANCE` from this value after #4530 + // is merged. + sched0.locked() + sched1.locked() - MINIMUM_BALANCE + ); + }); + } + + #[test] + fn merge_finished_and_yet_to_be_started_schedules() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // 20 block duration. + 10, // Ends at block 30 + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 30, + MINIMUM_BALANCE * 2, // 30 block duration. + 35, + ); + assert_ok!(AssetsVesting::vested_transfer(Some(13).into(), ASSET_ID, 2, sched1)); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1] + ); + + let sched2 = VestingInfo::new( + MINIMUM_BALANCE * 40, + MINIMUM_BALANCE, // 40 block duration. + 30, + ); + // Add a 3rd schedule to demonstrate how sched1 shifts. + assert_ok!(AssetsVesting::vested_transfer(Some(13).into(), ASSET_ID, 2, sched2)); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched1, sched2] + ); + + System::set_block_number(30); + + // At block 30, sched0 has finished unlocking while sched1 and sched2 are still + // fully locked, + assert_eq!( + AssetsVesting::vesting_balance(ASSET_ID, &2), + Some(sched1.locked() + sched2.locked()) + ); + // but since we have not vested usable balance is still 0. + assert_eq!(Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), 0); + + // Merge schedule 0 and 1. + assert_ok!(AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1)); + + // sched0 is removed since it finished, and sched1 is removed and then pushed on the + // back because it is treated as the merged schedule + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched2, sched1] + ); + + // The usable balance is updated because merging fully unlocked sched0. + assert_eq!( + Assets::reducible_balance(ASSET_ID, &2, Preserve, Polite), + // TODO: Remove the subtract of `MINIMUM_BALANCE` from this value after #4530 + // is merged. + sched0.locked() - MINIMUM_BALANCE + ); + }); + } + + #[test] + fn merge_schedules_throws_proper_errors() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + // Account 2 should already have a vesting schedule. + let sched0 = VestingInfo::new( + MINIMUM_BALANCE * 20, + MINIMUM_BALANCE, // 20 block duration. + 10, + ); + assert_eq!(VestingStorage::::get(ASSET_ID, &2).unwrap(), vec![sched0]); + + // Account 2 only has 1 vesting schedule. + assert_noop!( + AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 1), + Error::::ScheduleIndexOutOfBounds + ); + + // Account 4 has 0 vesting schedules. + assert_eq!(VestingStorage::::get(ASSET_ID, &4), None); + assert_noop!( + AssetsVesting::merge_schedules(Some(4).into(), ASSET_ID, 0, 1), + Error::::NotVesting + ); + + // There are enough schedules to merge but an index is non-existent. + AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 2, sched0).unwrap(); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![sched0, sched0] + ); + assert_noop!( + AssetsVesting::merge_schedules(Some(2).into(), ASSET_ID, 0, 2), + Error::::ScheduleIndexOutOfBounds + ); + + // It is a storage noop with no errors if the indexes are the same. + assert_storage_noop!(AssetsVesting::merge_schedules( + Some(2).into(), + ASSET_ID, + 0, + 0 + ) + .unwrap()); + }); + } +} + +mod genesis_config { + use super::*; + + #[test] + fn generates_multiple_schedules_from_genesis_config() { + ExtBuilder::default() + .with_asset( + ASSET_ID, + 1, + MINIMUM_BALANCE, + vec![ + (1, 10 * MINIMUM_BALANCE), + (2, 20 * MINIMUM_BALANCE), + (12, 10 * MINIMUM_BALANCE), + ], + ) + // 5 * existential deposit locked. + .with_vesting_genesis_config((ASSET_ID, 1, 0, 10, 5 * MINIMUM_BALANCE)) + // 1 * existential deposit locked. + .with_vesting_genesis_config((ASSET_ID, 2, 10, 20, 19 * MINIMUM_BALANCE)) + // 2 * existential deposit locked. + .with_vesting_genesis_config((ASSET_ID, 2, 10, 20, 18 * MINIMUM_BALANCE)) + // 1 * existential deposit locked. + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, 9 * MINIMUM_BALANCE)) + // 2 * existential deposit locked. + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, 8 * MINIMUM_BALANCE)) + // 3 * existential deposit locked. + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, 7 * MINIMUM_BALANCE)) + .build() + .execute_with(|| { + let user1_sched1 = VestingInfo::new(5 * MINIMUM_BALANCE, 128, 0u64); + assert_eq!(VestingStorage::::get(ASSET_ID, &1).unwrap(), vec![user1_sched1]); + + let user2_sched1 = VestingInfo::new(1 * MINIMUM_BALANCE, 12, 10u64); + let user2_sched2 = VestingInfo::new(2 * MINIMUM_BALANCE, 25, 10u64); + assert_eq!( + VestingStorage::::get(ASSET_ID, &2).unwrap(), + vec![user2_sched1, user2_sched2] + ); + + let user12_sched1 = VestingInfo::new(1 * MINIMUM_BALANCE, 12, 10u64); + let user12_sched2 = VestingInfo::new(2 * MINIMUM_BALANCE, 25, 10u64); + let user12_sched3 = VestingInfo::new(3 * MINIMUM_BALANCE, 38, 10u64); + assert_eq!( + VestingStorage::::get(ASSET_ID, &12).unwrap(), + vec![user12_sched1, user12_sched2, user12_sched3] + ); + }); + } + + #[test] + #[should_panic(expected = "Too many vesting schedules at genesis.: ()")] + fn multiple_schedules_from_genesis_config_errors() { + // MaxVestingSchedules is 3, but this config has 4 for account 12 so we panic when building + // from genesis. + ExtBuilder::default() + .with_asset(ASSET_ID, 1, MINIMUM_BALANCE, vec![(12, 5 * MINIMUM_BALANCE)]) + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, MINIMUM_BALANCE)) + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, MINIMUM_BALANCE)) + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, MINIMUM_BALANCE)) + .with_vesting_genesis_config((ASSET_ID, 12, 10, 20, MINIMUM_BALANCE)) + .build(); + } +} + +mod vesting_info { + use super::*; + + #[test] + fn merge_vesting_handles_per_block_0() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + let sched0 = VestingInfo::new( + MINIMUM_BALANCE, + 0, // Vesting over 256 blocks. + 1, + ); + assert_eq!(sched0.ending_block_as_balance::(), 257); + let sched1 = VestingInfo::new( + MINIMUM_BALANCE * 2, + 0, // Vesting over 512 blocks. + 10, + ); + assert_eq!(sched1.ending_block_as_balance::(), 512u64 + 10); + + let merged = VestingInfo::new(764, 1, 10); + assert_eq!(AssetsVesting::merge_vesting_info(5, sched0, sched1), Some(merged)); + }); + } + + #[test] + fn vesting_info_validate_works() { + let min_transfer = MIN_VESTED_TRANSFER; + // Does not check for min transfer. + assert_eq!(VestingInfo::new(min_transfer - 1, 1u64, 10u64).is_valid(), true); + + // `locked` cannot be 0. + assert_eq!(VestingInfo::new(0, 1u64, 10u64).is_valid(), false); + + // `per_block` cannot be 0. + assert_eq!(VestingInfo::new(min_transfer + 1, 0u64, 10u64).is_valid(), false); + + // With valid inputs it does not error. + assert_eq!(VestingInfo::new(min_transfer, 1u64, 10u64).is_valid(), true); + } + + #[test] + fn vesting_info_ending_block_as_balance_works() { + // Treats `per_block` 0 as 1. + let per_block_0 = VestingInfo::new(256u32, 0u32, 10u32); + assert_eq!(per_block_0.ending_block_as_balance::(), 256 + 10); + + // `per_block >= locked` always results in a schedule ending the block after it starts + let per_block_gt_locked = VestingInfo::new(256u32, 256 * 2u32, 10u32); + assert_eq!( + per_block_gt_locked.ending_block_as_balance::(), + 1 + per_block_gt_locked.starting_block() + ); + let per_block_eq_locked = VestingInfo::new(256u32, 256u32, 10u32); + assert_eq!( + per_block_gt_locked.ending_block_as_balance::(), + per_block_eq_locked.ending_block_as_balance::() + ); + + // Correctly calcs end if `locked % per_block != 0`. (We need a block to unlock the + // remainder). + let imperfect_per_block = VestingInfo::new(256u32, 250u32, 10u32); + assert_eq!( + imperfect_per_block.ending_block_as_balance::(), + imperfect_per_block.starting_block() + 2u32, + ); + assert_eq!( + imperfect_per_block + .locked_at::(imperfect_per_block.ending_block_as_balance::()), + 0 + ); + } + + #[test] + fn per_block_works() { + let per_block_0 = VestingInfo::new(256u32, 0u32, 10u32); + assert_eq!(per_block_0.per_block(), 1u32); + assert_eq!(per_block_0.raw_per_block(), 0u32); + + let per_block_1 = VestingInfo::new(256u32, 1u32, 10u32); + assert_eq!(per_block_1.per_block(), 1u32); + assert_eq!(per_block_1.raw_per_block(), 1u32); + } +} + +// When an accounts free balance + schedule.locked is less than ED, the vested transfer will fail. +#[test] +fn vested_transfer_less_than_existential_deposit_fails() { + use frame::traits::fungibles::Inspect; + + ExtBuilder::default() + .with_min_balance(ASSET_ID, 4 * MINIMUM_BALANCE) + .build() + .execute_with(|| { + // MinVestedTransfer is less the ED. + assert!(Assets::minimum_balance(ASSET_ID) > MIN_VESTED_TRANSFER); + + let sched = VestingInfo::new(MIN_VESTED_TRANSFER, 1u64, 10u64); + // The new account balance with the schedule's locked amount would be less than ED. + assert!( + Assets::balance(ASSET_ID, &99) + sched.locked() < Assets::minimum_balance(ASSET_ID) + ); + + // vested_transfer fails. + assert_noop!( + AssetsVesting::vested_transfer(Some(3).into(), ASSET_ID, 99, sched), + TokenError::BelowMinimum, + ); + // force_vested_transfer fails. + assert_noop!( + AssetsVesting::force_vested_transfer( + RawOrigin::Root.into(), + ASSET_ID, + 3, + 99, + sched + ), + TokenError::BelowMinimum, + ); + }); +} + +#[test] +fn remove_vesting_schedule() { + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + assert_eq!(Assets::balance(ASSET_ID, &3), 256 * 30); + assert_eq!(Assets::balance(ASSET_ID, &4), 256 * 40); + // Account 4 should not have any vesting yet. + assert_eq!(VestingStorage::::get(ASSET_ID, &4), None); + // Make the schedule for the new transfer. + let new_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + (MINIMUM_BALANCE * 5) / 20, // Vesting over 20 blocks + 10, + ); + assert_ok!(AssetsVesting::vested_transfer( + Some(3).into(), + ASSET_ID, + 4, + new_vesting_schedule + )); + // Now account 4 should have vesting. + assert_eq!( + VestingStorage::::get(ASSET_ID, &4).unwrap(), + vec![new_vesting_schedule] + ); + // Account 4 has 5 * 256 locked. + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(256 * 5)); + // Verify only root can call. + assert_noop!( + AssetsVesting::force_remove_vesting_schedule(Some(4).into(), ASSET_ID, 4, 0), + BadOrigin + ); + // Verify that root can remove the schedule. + assert_ok!(AssetsVesting::force_remove_vesting_schedule( + RawOrigin::Root.into(), + ASSET_ID, + 4, + 0 + )); + // Verify that last event is VestingCompleted. + System::assert_last_event( + crate::Event::::VestingCompleted { asset: ASSET_ID, account: 4 }.into(), + ); + // Appropriate storage is cleaned up. + assert!(!>::contains_key(ASSET_ID, 4)); + // Check the vesting balance is zero. + assert_eq!(VestingStorage::::get(ASSET_ID, &4), None); + // Verifies that trying to remove a schedule when it doesnt exist throws error. + assert_noop!( + AssetsVesting::force_remove_vesting_schedule( + RawOrigin::Root.into(), + ASSET_ID, + 4, + 0 + ), + Error::::InvalidScheduleParams + ); + }); +} + +#[test] +fn vested_transfer_impl_works() { + use frame::traits::fungibles::VestedTransfer; + + ExtBuilder::default() + .with_min_balance(ASSET_ID, MINIMUM_BALANCE) + .build() + .execute_with(|| { + assert_eq!(Assets::balance(ASSET_ID, &3), 256 * 30); + assert_eq!(Assets::balance(ASSET_ID, &4), 256 * 40); + // Account 4 should not have any vesting yet. + assert_eq!(VestingStorage::::get(ASSET_ID, &4), None); + + // Basic working scenario + assert_ok!(>::vested_transfer( + ASSET_ID, + &3, + &4, + MINIMUM_BALANCE * 5, + MINIMUM_BALANCE * 5 / 20, + 10 + )); + // Now account 4 should have vesting. + let new_vesting_schedule = VestingInfo::new( + MINIMUM_BALANCE * 5, + (MINIMUM_BALANCE * 5) / 20, // Vesting over 20 blocks + 10, + ); + assert_eq!( + VestingStorage::::get(ASSET_ID, &4).unwrap(), + vec![new_vesting_schedule] + ); + // Account 4 has 5 * 256 locked. + assert_eq!(AssetsVesting::vesting_balance(ASSET_ID, &4), Some(256 * 5)); + + // If the transfer fails (because they don't have enough balance), no storage is + // changed. + assert_noop!( + >::vested_transfer( + ASSET_ID, + &3, + &4, + MINIMUM_BALANCE * 9999, + MINIMUM_BALANCE * 5 / 20, + 10 + ), + TokenError::FundsUnavailable + ); + + // If applying the vesting schedule fails (per block is 0), no storage is changed. + assert_noop!( + >::vested_transfer( + ASSET_ID, + &3, + &4, + MINIMUM_BALANCE * 5, + 0, + 10 + ), + Error::::InvalidScheduleParams + ); + }); +} diff --git a/substrate/frame/assets-vesting/src/types.rs b/substrate/frame/assets-vesting/src/types.rs new file mode 100644 index 0000000000000..72e3520725c82 --- /dev/null +++ b/substrate/frame/assets-vesting/src/types.rs @@ -0,0 +1,166 @@ +// This file is part of Substrate. + +// 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. + +//! Module to manage types related to vesting. + +use super::*; + +pub(crate) type AccountIdOf = ::AccountId; +pub(crate) type AssetIdOf = + <>::Assets as Inspect>>::AssetId; +pub(crate) type BalanceOf = + <>::Assets as Inspect>>::Balance; +pub(crate) type AccountIdLookupOf = + <::Lookup as StaticLookup>::Source; + +/// Actions to take against a user's `Vesting` storage entry. +#[derive(Clone, Copy)] +pub(crate) enum VestingAction { + /// Do not actively remove any schedules. + Passive, + /// Remove the schedule specified by the index. + Remove { index: usize }, + /// Remove the two schedules, specified by index, so they can be merged. + Merge { index1: usize, index2: usize }, +} + +impl VestingAction { + /// Whether or not the filter says the schedule index should be removed. + pub(crate) fn should_remove(&self, index: usize) -> bool { + match self { + Self::Passive => false, + Self::Remove { index: index1 } => *index1 == index, + Self::Merge { index1, index2 } => *index1 == index || *index2 == index, + } + } + + /// Pick the schedules that this action dictates should continue vesting undisturbed. + pub(crate) fn pick_schedules, I: 'static>( + &self, + schedules: Vec, BlockNumberFor>>, + ) -> impl Iterator, BlockNumberFor>> + '_ { + schedules.into_iter().enumerate().filter_map(move |(index, schedule)| { + if self.should_remove(index) { + None + } else { + Some(schedule) + } + }) + } +} + +// Wrapper for `T::MAX_VESTING_SCHEDULES` to satisfy `trait Get`. +pub struct MaxVestingSchedulesGet(PhantomData<(T, I)>); +impl, I: 'static> Get for MaxVestingSchedulesGet { + fn get() -> u32 { + T::MAX_VESTING_SCHEDULES + } +} + +/// Struct to encode the vesting schedule of an individual account. +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct VestingInfo { + /// Locked amount at genesis. + frozen: Balance, + /// Amount that gets unlocked every block after `starting_block`. + per_block: Balance, + /// Starting block for unlocking(vesting). + starting_block: BlockNumber, +} + +impl VestingInfo +where + Balance: AtLeast32BitUnsigned + Copy, + BlockNumber: AtLeast32BitUnsigned + Copy + Bounded, +{ + /// Instantiate a new `VestingInfo`. + pub fn new( + frozen: Balance, + per_block: Balance, + starting_block: BlockNumber, + ) -> VestingInfo { + VestingInfo { frozen, per_block, starting_block } + } + + /// Validate parameters for `VestingInfo`. Note that this does not check + /// against `MinVestedTransfer`. + pub fn is_valid(&self) -> bool { + !self.frozen.is_zero() && !self.raw_per_block().is_zero() + } + + /// Locked amount at schedule creation. + pub fn locked(&self) -> Balance { + self.frozen + } + + /// Amount that gets thawed every block after `starting_block`. Corrects for `per_block` of 0. + /// We don't let `per_block` be less than 1, or else the vesting will never end. + /// This should be used whenever accessing `per_block` unless explicitly checking for 0 values. + pub fn per_block(&self) -> Balance { + self.per_block.max(One::one()) + } + + /// Get the unmodified `per_block`. Generally should not be used, but is useful for + /// validating `per_block`. + pub(crate) fn raw_per_block(&self) -> Balance { + self.per_block + } + + /// Starting block for thawing(vesting). + pub fn starting_block(&self) -> BlockNumber { + self.starting_block + } + + /// Amount frozen at block `n`. + pub fn locked_at>( + &self, + n: BlockNumber, + ) -> Balance { + // Number of blocks that count toward vesting; + // saturating to 0 when n < starting_block. + let vested_block_count = n.saturating_sub(self.starting_block); + let vested_block_count = BlockNumberToBalance::convert(vested_block_count); + // Return amount that is still frozen in vesting. + vested_block_count + .checked_mul(&self.per_block()) // `per_block` accessor guarantees at least 1. + .map(|to_unlock| self.frozen.saturating_sub(to_unlock)) + .unwrap_or(Zero::zero()) + } + + /// Block number at which the schedule ends (as type `Balance`). + pub fn ending_block_as_balance>( + &self, + ) -> Balance { + let starting_block = BlockNumberToBalance::convert(self.starting_block); + let duration = if self.per_block() >= self.frozen { + // If `per_block` is bigger than `frozen`, the schedule will end + // the block after starting. + One::one() + } else { + self.frozen / self.per_block() + + if (self.frozen % self.per_block()).is_zero() { + Zero::zero() + } else { + // `per_block` does not perfectly divide `frozen`, so we need an extra block to + // thaw some amount less than `per_block`. + One::one() + } + }; + + starting_block.saturating_add(duration) + } +} diff --git a/substrate/frame/assets-vesting/src/weights.rs b/substrate/frame/assets-vesting/src/weights.rs new file mode 100644 index 0000000000000..5c626ee2cffdb --- /dev/null +++ b/substrate/frame/assets-vesting/src/weights.rs @@ -0,0 +1,412 @@ +// This file is part of Substrate. + +// 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. + +//! Weights for `pallet_vesting` + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame::traits::Get; +use core::marker::PhantomData; +use frame::deps::frame_system; +use frame::weights_prelude::{RocksDbWeight, Weight}; + +/// Weight functions needed for `pallet_vesting`. +pub trait WeightInfo { + fn vest_locked(s: u32, ) -> Weight; + fn vest_unlocked(s: u32, ) -> Weight; + fn vest_other_locked(s: u32, ) -> Weight; + fn vest_other_unlocked(s: u32, ) -> Weight; + fn vested_transfer(s: u32, ) -> Weight; + fn force_vested_transfer(s: u32, ) -> Weight; + fn not_unlocking_merge_schedules(s: u32, ) -> Weight; + fn unlocking_merge_schedules( s: u32, ) -> Weight; + fn force_remove_vesting_schedule(s: u32, ) -> Weight; +} + +/// Weights for `pallet_vesting` using the Substrate node and recommended hardware. +pub struct SubstrateWeight(PhantomData); +impl WeightInfo for SubstrateWeight { + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 vest_locked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 39_505_000 picoseconds. + Weight::from_parts(39_835_306, 4764) + // Standard Error: 2_481 + .saturating_add(Weight::from_parts(70_901, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 vest_unlocked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 40_781_000 picoseconds. + Weight::from_parts(40_777_528, 4764) + // Standard Error: 2_151 + .saturating_add(Weight::from_parts(83_093, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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`) + /// 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, 28]`. + fn vest_other_locked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `517 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 41_590_000 picoseconds. + Weight::from_parts(40_756_231, 4764) + // Standard Error: 2_527 + .saturating_add(Weight::from_parts(102_603, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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`) + /// 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, 28]`. + fn vest_other_unlocked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `517 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 43_490_000 picoseconds. + Weight::from_parts(43_900_384, 4764) + // Standard Error: 2_971 + .saturating_add(Weight::from_parts(66_673, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// 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:1 w:1) + /// 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 vested_transfer(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `588 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 76_194_000 picoseconds. + Weight::from_parts(77_923_603, 4764) + // Standard Error: 3_810 + .saturating_add(Weight::from_parts(97_415, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } + /// 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 force_vested_transfer(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `691 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `6196` + // Minimum execution time: 78_333_000 picoseconds. + Weight::from_parts(80_199_350, 6196) + // Standard Error: 3_385 + .saturating_add(Weight::from_parts(106_311, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 `[2, 28]`. + fn not_unlocking_merge_schedules(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 40_102_000 picoseconds. + Weight::from_parts(39_552_301, 4764) + // Standard Error: 2_418 + .saturating_add(Weight::from_parts(91_621, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 `[2, 28]`. + fn unlocking_merge_schedules(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 42_287_000 picoseconds. + Weight::from_parts(41_937_484, 4764) + // Standard Error: 2_412 + .saturating_add(Weight::from_parts(85_247, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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`) + /// 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, 28]`. + fn force_remove_vesting_schedule(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `588 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 46_462_000 picoseconds. + Weight::from_parts(46_571_504, 4764) + // Standard Error: 2_397 + .saturating_add(Weight::from_parts(77_382, 0).saturating_mul(s.into())) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(3_u64)) + } +} + +// For backwards compatibility and tests. +impl WeightInfo for () { + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 vest_locked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 39_505_000 picoseconds. + Weight::from_parts(39_835_306, 4764) + // Standard Error: 2_481 + .saturating_add(Weight::from_parts(70_901, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 vest_unlocked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 40_781_000 picoseconds. + Weight::from_parts(40_777_528, 4764) + // Standard Error: 2_151 + .saturating_add(Weight::from_parts(83_093, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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`) + /// 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, 28]`. + fn vest_other_locked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `517 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 41_590_000 picoseconds. + Weight::from_parts(40_756_231, 4764) + // Standard Error: 2_527 + .saturating_add(Weight::from_parts(102_603, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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`) + /// 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, 28]`. + fn vest_other_unlocked(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `517 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 43_490_000 picoseconds. + Weight::from_parts(43_900_384, 4764) + // Standard Error: 2_971 + .saturating_add(Weight::from_parts(66_673, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// 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:1 w:1) + /// 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 vested_transfer(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `588 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 76_194_000 picoseconds. + Weight::from_parts(77_923_603, 4764) + // Standard Error: 3_810 + .saturating_add(Weight::from_parts(97_415, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } + /// 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 force_vested_transfer(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `691 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `6196` + // Minimum execution time: 78_333_000 picoseconds. + Weight::from_parts(80_199_350, 6196) + // Standard Error: 3_385 + .saturating_add(Weight::from_parts(106_311, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(5_u64)) + .saturating_add(RocksDbWeight::get().writes(4_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 `[2, 28]`. + fn not_unlocking_merge_schedules(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 40_102_000 picoseconds. + Weight::from_parts(39_552_301, 4764) + // Standard Error: 2_418 + .saturating_add(Weight::from_parts(91_621, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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 `[2, 28]`. + fn unlocking_merge_schedules(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `414 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 42_287_000 picoseconds. + Weight::from_parts(41_937_484, 4764) + // Standard Error: 2_412 + .saturating_add(Weight::from_parts(85_247, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(3_u64)) + .saturating_add(RocksDbWeight::get().writes(2_u64)) + } + /// Storage: `Vesting::Vesting` (r:1 w:1) + /// Proof: `Vesting::Vesting` (`max_values`: None, `max_size`: Some(1057), added: 3532, 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`) + /// 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, 28]`. + fn force_remove_vesting_schedule(s: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `588 + l * (25 ±0) + s * (36 ±0)` + // Estimated: `4764` + // Minimum execution time: 46_462_000 picoseconds. + Weight::from_parts(46_571_504, 4764) + // Standard Error: 2_397 + .saturating_add(Weight::from_parts(77_382, 0).saturating_mul(s.into())) + .saturating_add(RocksDbWeight::get().reads(4_u64)) + .saturating_add(RocksDbWeight::get().writes(3_u64)) + } +} diff --git a/substrate/frame/support/src/traits/tokens/fungible/mod.rs b/substrate/frame/support/src/traits/tokens/fungible/mod.rs index c67755e133bf4..ea3b7d4256a98 100644 --- a/substrate/frame/support/src/traits/tokens/fungible/mod.rs +++ b/substrate/frame/support/src/traits/tokens/fungible/mod.rs @@ -159,6 +159,7 @@ pub(crate) mod imbalance; mod item_of; mod regular; mod union_of; +mod vesting; use codec::{Decode, Encode, MaxEncodedLen}; use core::marker::PhantomData; @@ -185,6 +186,7 @@ use sp_arithmetic::traits::Zero; use sp_core::Get; use sp_runtime::{traits::Convert, DispatchError}; pub use union_of::{NativeFromLeft, NativeOrWithId, UnionOf}; +pub use vesting::{VestedTransfer, VestingSchedule}; #[cfg(feature = "experimental")] use crate::traits::MaybeConsideration; diff --git a/substrate/frame/support/src/traits/tokens/fungible/vesting.rs b/substrate/frame/support/src/traits/tokens/fungible/vesting.rs new file mode 100644 index 0000000000000..8273422e492e9 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungible/vesting.rs @@ -0,0 +1,116 @@ +// This file is part of Substrate. + +// 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. + +//! `VestingSchedule` and `VestedTransfer` traits for working with vesting schedules. +//! +//! See the [`crate::traits::fungible`] doc for more information about fungible traits. + +use crate::dispatch::DispatchResult; + +/// A vesting schedule over a fungible type. This allows a particular currency to have vesting +/// limits applied to it. +pub trait VestingSchedule { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// The balance type that this schedule applies to. + type Balance; + + /// Get the amount that is currently being vested and cannot be transferred out of this account. + /// Returns `None` if the account has no vesting schedule. + fn vesting_balance(who: &AccountId) -> Option; + + /// Adds a vesting schedule to a given account. + /// + /// If the account has `MaxVestingSchedules`, an Error is returned and nothing + /// is updated. + /// + /// Is a no-op if the amount to be vested is zero. + /// + /// NOTE: This doesn't alter the free balance of the account. + fn add_vesting_schedule( + who: &AccountId, + locked: Self::Balance, + per_block: Self::Balance, + starting_block: Self::Moment, + ) -> DispatchResult; + + /// Checks if `add_vesting_schedule` would work against `who`. + fn can_add_vesting_schedule( + who: &AccountId, + locked: Self::Balance, + per_block: Self::Balance, + starting_block: Self::Moment, + ) -> DispatchResult; + + /// Remove a vesting schedule for a given account. + /// + /// NOTE: This doesn't alter the free balance of the account. + fn remove_vesting_schedule(who: &AccountId, schedule_index: u32) -> DispatchResult; +} + +/// A vested transfer over a token. This allows a transferred amount to vest over time. +pub trait VestedTransfer { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// The balance type that this schedule applies to. + type Balance; + + /// Execute a vested transfer from `source` to `target` with the given schedule: + /// - `frozen`: The amount of tokens to be transferred and for the vesting schedule to apply + /// to. + /// - `per_block`: The amount to be unlocked each block. (linear vesting) + /// - `starting_block`: The block where the vesting should start. This block can be in the past + /// or future, and should adjust when the balance become available to the user. + /// + /// Example: Assume we are on block 100. If `frozen` amount is 100, and `per_block` is 1: + /// - If `starting_block` is 0, then the whole 100 tokens will be available right away as the + /// vesting schedule started in the past and has fully completed. + /// - If `starting_block` is 50, then 50 tokens are made available right away, and 50 more + /// tokens will unlock one token at a time until block 150. + /// - If `starting_block` is 100, then each block, 1 tokens will be unlocked until the whole + /// balance is unlocked at block 200. + /// - If `starting_block` is 200, then the 100 token balance will be completely locked until + /// block 200, and then start to unlock one token at a time until block 300. + fn vested_transfer( + source: &AccountId, + target: &AccountId, + locked: Self::Balance, + per_block: Self::Balance, + starting_block: Self::Moment, + ) -> DispatchResult; +} + +// A no-op implementation of `VestedTransfer` for pallets that require this trait, but users may +// not want to implement this functionality +pub struct NoVestedTransfers(core::marker::PhantomData); + +impl VestedTransfer for NoVestedTransfers { + type Moment = (); + type Balance = Balance; + + fn vested_transfer( + _source: &AccountId, + _target: &AccountId, + _locked: Self::Balance, + _per_block: Self::Balance, + _starting_block: Self::Moment, + ) -> DispatchResult { + Err(sp_runtime::DispatchError::Unavailable.into()) + } +} diff --git a/substrate/frame/support/src/traits/tokens/fungibles/mod.rs b/substrate/frame/support/src/traits/tokens/fungibles/mod.rs index 8b4ea4d13cf9d..c4ce646a76195 100644 --- a/substrate/frame/support/src/traits/tokens/fungibles/mod.rs +++ b/substrate/frame/support/src/traits/tokens/fungibles/mod.rs @@ -36,6 +36,7 @@ pub mod metadata; mod regular; pub mod roles; mod union_of; +mod vesting; pub use enumerable::Inspect as InspectEnumerable; pub use freeze::{Inspect as InspectFreeze, Mutate as MutateFreeze}; @@ -49,3 +50,4 @@ pub use regular::{ Balanced, DecreaseIssuance, Dust, IncreaseIssuance, Inspect, Mutate, Unbalanced, }; pub use union_of::UnionOf; +pub use vesting::{VestedTransfer, VestingSchedule}; diff --git a/substrate/frame/support/src/traits/tokens/fungibles/vesting.rs b/substrate/frame/support/src/traits/tokens/fungibles/vesting.rs new file mode 100644 index 0000000000000..846a7f957f477 --- /dev/null +++ b/substrate/frame/support/src/traits/tokens/fungibles/vesting.rs @@ -0,0 +1,131 @@ +// This file is part of Substrate. + +// 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. + +//! `VestingSchedule` and `VestedTransfer` traits for working with vesting schedules. +//! +//! See the [`crate::traits::fungibles`] doc for more information about fungibles traits. + +use crate::{dispatch::DispatchResult, traits::tokens::misc::AssetId}; + +/// A vesting schedule over a fungible asset class. This allows a particular currency to have +/// vesting limits applied to it. +pub trait VestingSchedule { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// Means of identifying one asset class from another. + type AssetId: AssetId; + + /// The balance type that this schedule applies to. + type Balance; + + /// Get the amount that is currently being vested and cannot be transferred out of this asset + /// account. Returns `None` if the asset account has no vesting schedule. + fn vesting_balance(asset: Self::AssetId, who: &AccountId) -> Option; + + /// Adds a vesting schedule to a given asset account. + /// + /// If the account has `MaxVestingSchedules`, an Error is returned and nothing + /// is updated. + /// + /// Is a no-op if the amount to be vested is zero. + /// + /// NOTE: This doesn't alter the free balance of the asset account. + fn add_vesting_schedule( + asset: Self::AssetId, + who: &AccountId, + locked: Self::Balance, + per_block: Self::Balance, + starting_block: Self::Moment, + ) -> DispatchResult; + + /// Checks if `add_vesting_schedule` would work against `who`. + fn can_add_vesting_schedule( + asset: Self::AssetId, + who: &AccountId, + locked: Self::Balance, + per_block: Self::Balance, + starting_block: Self::Moment, + ) -> DispatchResult; + + /// Remove a vesting schedule for a given asset account. + /// + /// NOTE: This doesn't alter the free balance of the asset account. + fn remove_vesting_schedule( + asset: Self::AssetId, + who: &AccountId, + schedule_index: u32, + ) -> DispatchResult; +} + +/// A vested transfer over an asset. This allows a transferred amount to vest over time. +pub trait VestedTransfer { + /// The quantity used to denote time; usually just a `BlockNumber`. + type Moment; + + /// Means of identifying one asset class from another. + type AssetId: AssetId; + + /// The balance type that this schedule applies to. + type Balance; + + /// Execute a vested transfer from `source` to `target` with the given schedule: + /// - `frozen`: The amount of assets to be transferred and for the vesting schedule to apply + /// to. + /// - `per_block`: The amount to be unlocked each block. (linear vesting) + /// - `starting_block`: The block where the vesting should start. This block can be in the past + /// or future, and should adjust when the balance become available to the user. + /// + /// Example: Assume we are on block 100. If `frozen` amount is 100, and `per_block` is 1: + /// - If `starting_block` is 0, then the whole 100 tokens will be available right away as the + /// vesting schedule started in the past and has fully completed. + /// - If `starting_block` is 50, then 50 tokens are made available right away, and 50 more + /// tokens will unlock one token at a time until block 150. + /// - If `starting_block` is 100, then each block, 1 tokens will be unlocked until the whole + /// balance is unlocked at block 200. + /// - If `starting_block` is 200, then the 100 token balance will be completely locked until + /// block 200, and then start to unlock one token at a time until block 300. + fn vested_transfer( + asset: Self::AssetId, + source: &AccountId, + target: &AccountId, + locked: Self::Balance, + per_block: Self::Balance, + starting_block: Self::Moment, + ) -> DispatchResult; +} + +// A no-op implementation of `VestedTransfer` for pallets that require this trait, but users may +// not want to implement this functionality +pub struct NoVestedTransfers(core::marker::PhantomData<(A, B)>); + +impl VestedTransfer for NoVestedTransfers { + type Moment = (); + type AssetId = Id; + type Balance = Balance; + + fn vested_transfer( + _asset: Self::AssetId, + _source: &AccountId, + _target: &AccountId, + _locked: Self::Balance, + _per_block: Self::Balance, + _starting_block: Self::Moment, + ) -> DispatchResult { + Err(sp_runtime::DispatchError::Unavailable.into()) + } +} diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index fc0b2d5a140ed..14993d37dca2c 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -60,6 +60,7 @@ std = [ "pallet-asset-rewards?/std", "pallet-asset-tx-payment?/std", "pallet-assets-freezer?/std", + "pallet-assets-vesting?/std", "pallet-assets?/std", "pallet-atomic-swap?/std", "pallet-aura?/std", @@ -260,6 +261,7 @@ runtime-benchmarks = [ "pallet-asset-rewards?/runtime-benchmarks", "pallet-asset-tx-payment?/runtime-benchmarks", "pallet-assets-freezer?/runtime-benchmarks", + "pallet-assets-vesting?/runtime-benchmarks", "pallet-assets?/runtime-benchmarks", "pallet-babe?/runtime-benchmarks", "pallet-bags-list?/runtime-benchmarks", @@ -391,6 +393,7 @@ try-runtime = [ "pallet-asset-rewards?/try-runtime", "pallet-asset-tx-payment?/try-runtime", "pallet-assets-freezer?/try-runtime", + "pallet-assets-vesting?/try-runtime", "pallet-assets?/try-runtime", "pallet-atomic-swap?/try-runtime", "pallet-aura?/try-runtime", @@ -546,7 +549,7 @@ with-tracing = [ "sp-tracing?/with-tracing", "sp-tracing?/with-tracing", ] -runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-weight-reclaim", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-rewards", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"] +runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-weight-reclaim", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-rewards", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-assets-vesting", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"] runtime = [ "frame-benchmarking", "frame-benchmarking-pallet-pov", @@ -893,6 +896,11 @@ default-features = false optional = true path = "../substrate/frame/assets-freezer" +[dependencies.pallet-assets-vesting] +default-features = false +optional = true +path = "../substrate/frame/assets-vesting" + [dependencies.pallet-atomic-swap] default-features = false optional = true diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index a132f16a2c33f..8724869b40e37 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -328,6 +328,10 @@ pub use pallet_assets; #[cfg(feature = "pallet-assets-freezer")] pub use pallet_assets_freezer; +/// FRAME pallet for manage vesting on Assets. +#[cfg(feature = "pallet-assets-vesting")] +pub use pallet_assets_vesting; + /// FRAME atomic swap pallet. #[cfg(feature = "pallet-atomic-swap")] pub use pallet_atomic_swap;