diff --git a/Cargo.lock b/Cargo.lock index 03350ff38f5d..6ce6a293db5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10948,6 +10948,23 @@ dependencies = [ "sp-runtime 31.0.1", ] +[[package]] +name = "pallet-assets-holder" +version = "0.1.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "log", + "pallet-assets", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core 28.0.0", + "sp-io 30.0.0", + "sp-runtime 31.0.1", +] + [[package]] name = "pallet-atomic-swap" version = "28.0.0" @@ -15445,6 +15462,7 @@ dependencies = [ "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", + "pallet-assets-holder", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", diff --git a/Cargo.toml b/Cargo.toml index b12469987ed6..39008599fb18 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -315,6 +315,7 @@ members = [ "substrate/frame/asset-rate", "substrate/frame/assets", "substrate/frame/assets-freezer", + "substrate/frame/assets-holder", "substrate/frame/atomic-swap", "substrate/frame/aura", "substrate/frame/authority-discovery", @@ -894,6 +895,7 @@ pallet-asset-rate = { path = "substrate/frame/asset-rate", default-features = fa 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-holder = { path = "substrate/frame/assets-holder", 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/src/lib.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs index 2f9d83bd9d0b..3a83998fe791 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs @@ -265,6 +265,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = MetadataDepositPerByte; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = AssetsFreezer; type Extra = (); type WeightInfo = weights::pallet_assets_local::WeightInfo; @@ -310,6 +311,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ConstU128<0>; type ApprovalDeposit = ApprovalDeposit; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = PoolAssetsFreezer; type Extra = (); type WeightInfo = weights::pallet_assets_pool::WeightInfo; @@ -434,6 +436,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ForeignAssetsMetadataDepositPerByte; type ApprovalDeposit = ForeignAssetsApprovalDeposit; type StringLimit = ForeignAssetsAssetsStringLimit; + type Holder = (); type Freezer = ForeignAssetsFreezer; type Extra = (); type WeightInfo = weights::pallet_assets_foreign::WeightInfo; 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 63175222cc26..dab39043b641 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs @@ -268,6 +268,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = MetadataDepositPerByte; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = AssetsFreezer; type Extra = (); type WeightInfo = weights::pallet_assets_local::WeightInfo; @@ -312,6 +313,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ConstU128<0>; type ApprovalDeposit = ConstU128<0>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = PoolAssetsFreezer; type Extra = (); type WeightInfo = weights::pallet_assets_pool::WeightInfo; @@ -436,6 +438,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ForeignAssetsMetadataDepositPerByte; type ApprovalDeposit = ForeignAssetsApprovalDeposit; type StringLimit = ForeignAssetsAssetsStringLimit; + type Holder = (); type Freezer = ForeignAssetsFreezer; type Extra = (); type WeightInfo = weights::pallet_assets_foreign::WeightInfo; diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs index b51670c792d6..d89737aef1b7 100644 --- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs @@ -467,6 +467,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = MetadataDepositPerByte; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = pallet_assets::weights::SubstrateWeight; @@ -505,6 +506,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ForeignAssetsMetadataDepositPerByte; type ApprovalDeposit = ForeignAssetsApprovalDeposit; type StringLimit = ForeignAssetsAssetsStringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = pallet_assets::weights::SubstrateWeight; @@ -544,6 +546,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ConstU128<0>; type ApprovalDeposit = ConstU128<0>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = pallet_assets::weights::SubstrateWeight; diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs index 42556e0b493c..8b7edca48824 100644 --- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs +++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs @@ -592,6 +592,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = MetadataDepositPerByte; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = pallet_assets::weights::SubstrateWeight; diff --git a/polkadot/xcm/pallet-xcm/src/mock.rs b/polkadot/xcm/pallet-xcm/src/mock.rs index 8d0476b0e70d..56618a60b4f1 100644 --- a/polkadot/xcm/pallet-xcm/src/mock.rs +++ b/polkadot/xcm/pallet-xcm/src/mock.rs @@ -299,6 +299,7 @@ impl pallet_assets::Config for Test { type MetadataDepositPerByte = ConstU128<1>; type ApprovalDeposit = ConstU128<1>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = (); type WeightInfo = (); type CallbackHandle = (); diff --git a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs index e6fe8e45c265..209f0ca764c9 100644 --- a/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs +++ b/polkadot/xcm/xcm-builder/src/asset_exchange/single_asset_adapter/mock.rs @@ -82,6 +82,7 @@ impl pallet_assets::Config for Runtime { type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; type Freezer = (); + type Holder = (); type CallbackHandle = (); } @@ -97,6 +98,7 @@ impl pallet_assets::Config for Runtime { type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; type Freezer = (); + type Holder = (); type CallbackHandle = (); } diff --git a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs index 26ea226313f0..1d95a8395c2a 100644 --- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs +++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs @@ -111,6 +111,7 @@ impl pallet_assets::Config for Test { type AssetAccountDeposit = AssetAccountDeposit; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = (); diff --git a/polkadot/xcm/xcm-runtime-apis/tests/mock.rs b/polkadot/xcm/xcm-runtime-apis/tests/mock.rs index f0a5be908f69..e0bdbc9b084a 100644 --- a/polkadot/xcm/xcm-runtime-apis/tests/mock.rs +++ b/polkadot/xcm/xcm-runtime-apis/tests/mock.rs @@ -96,6 +96,7 @@ impl pallet_assets::Config for TestRuntime { type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; + type Holder = (); type Freezer = (); type AssetDeposit = ConstU128<1>; type AssetAccountDeposit = ConstU128<10>; diff --git a/prdoc/pr_4530.prdoc b/prdoc/pr_4530.prdoc new file mode 100644 index 000000000000..6984f861a76b --- /dev/null +++ b/prdoc/pr_4530.prdoc @@ -0,0 +1,107 @@ +title: "Implement `pallet-assets-holder`" + +doc: + - audience: Runtime Dev + description: | + This change creates the `pallet-assets-holder` pallet, as well as changes `pallet-assets` + to support querying held balances via a new trait: `BalanceOnHold`. + + ## Changes in Balance Model + + The change also adjusts the balance model implementation for fungible sets. This aligns the + calculation of the _spendable_ balance (that can be reduced either via withdrawals, like + paying for fees, or transfer to other accounts) to behave like it works with native tokens. + + As a consequence, when this change is introduced, adding freezes (a.k.a. locks) or balances + on hold (a.k.a. reserves) to an asset account will constraint the amount of balance for such + account that can be withdrawn or transferred, and will affect the ability for these accounts + to be destroyed. + + ### Example + + Before the changes in the balance model, an asset account balance could look like something like this: + + ``` + |____________balance____________| + |__frozen__| + |__ed__| + |___untouchable___|__spendable__| + ``` + + In the previous model, you could spend funds up to `ed + frozen` where `ed` is the minimum balance for an asset + class, and `frozen` is the frozen amount (if any `freezes` are in place). + + Now, the model looks like this: + + ``` + |__total__________________________________| + |__on_hold__|_____________free____________| + |__________frozen___________| + |__on_hold__|__ed__| + |__untouchable__|__spendable__| + ``` + + There's now a balance `on_hold` and a `free` balance. The balance `on_hold` is managed by a `Holder` (typically + `pallet-assets-holder`) and `free` is the balance that remains in `pallet-assets`. The `frozen` amount can be + subsumed into the balance `on_hold`, and now you can spend funds up to `max(frozen, ed)`, so if for an account, + `frozen` is less or equal than `on_hold + ed`, you'd be able to spend your `free` balance up to `ed`. If for + the account, `frozen` is more than `on_hold + ed`, the remaining amount after subtracting `frozen` to + `on_hold + ed` is the amount you cannot spend from your `free` balance. + + See [sdk docs](https://paritytech.github.io/polkadot-sdk/master/frame_support/traits/tokens/fungible/index.html#visualising-balance-components-together-) + to understand how to calculate the spendable balance of an asset account on the client side. + + ## Implementation of `InspectHold` and `MutateHold` + + The `pallet-assets-holder` implements `hold` traits for `pallet-assets`, by extending this + pallet and implementing the `BalanceOnHold` trait so the held balance can be queried by + `pallet-assets` to calculate the reducible (a.k.a. spendable) balance. + + These changes imply adding a configuration type in `pallet-assets` for `Holder` + + ## Default implementation of `Holder` + + Use `()` as the default value, when no holding capabilities are wanted in the runtime + implementation. + + ## Enable `pallet-assets-holder` + + Define an instance of `pallet-assets-holder` (we'll call it `AssetsHolder`) and use + `AssetsHolder` as the type for `Holder`, when intend to use holding capabilities are + wanted in the runtime implementation. + +crates: + - name: asset-hub-rococo-runtime + bump: minor + - name: asset-hub-westend-runtime + bump: minor + - name: pallet-asset-tx-payment + bump: patch + - name: pallet-asset-conversion-ops + bump: patch + - name: pallet-asset-conversion-tx-payment + bump: patch + - name: pallet-assets + bump: major + - name: pallet-assets-holder + bump: major + - name: pallet-assets-freezer + bump: patch + - name: pallet-contracts-mock-network + bump: patch + - name: pallet-nft-fractionalization + bump: patch + - name: pallet-revive-mock-network + bump: patch + - name: pallet-xcm + bump: patch + - name: penpal-runtime + bump: patch + - name: rococo-parachain-runtime + bump: patch + - name: polkadot-sdk + bump: minor + - name: staging-xcm-builder + bump: patch + - name: xcm-runtime-apis + bump: patch diff --git a/substrate/bin/node/runtime/src/lib.rs b/substrate/bin/node/runtime/src/lib.rs index 5a2ff3ceb7f6..0b5248f5b44f 100644 --- a/substrate/bin/node/runtime/src/lib.rs +++ b/substrate/bin/node/runtime/src/lib.rs @@ -1770,6 +1770,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = MetadataDepositPerByte; type ApprovalDeposit = ApprovalDeposit; type StringLimit = StringLimit; + type Holder = (); type Freezer = (); type Extra = (); type CallbackHandle = (); @@ -1797,6 +1798,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = MetadataDepositPerByte; type ApprovalDeposit = ApprovalDeposit; type StringLimit = StringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = pallet_assets::weights::SubstrateWeight; diff --git a/substrate/frame/asset-conversion/ops/src/mock.rs b/substrate/frame/asset-conversion/ops/src/mock.rs index 5c05faa6aa88..a932778b25d9 100644 --- a/substrate/frame/asset-conversion/ops/src/mock.rs +++ b/substrate/frame/asset-conversion/ops/src/mock.rs @@ -67,6 +67,7 @@ impl pallet_assets::Config for Test { type Currency = Balances; type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; + type Holder = (); type Freezer = (); } @@ -76,6 +77,7 @@ impl pallet_assets::Config for Test { type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; + type Holder = (); type Freezer = (); } diff --git a/substrate/frame/asset-conversion/src/mock.rs b/substrate/frame/asset-conversion/src/mock.rs index d8832d70488a..054473c3d29c 100644 --- a/substrate/frame/asset-conversion/src/mock.rs +++ b/substrate/frame/asset-conversion/src/mock.rs @@ -83,6 +83,7 @@ impl pallet_assets::Config for Test { type MetadataDepositPerByte = ConstU128<1>; type ApprovalDeposit = ConstU128<1>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = (); @@ -108,6 +109,7 @@ impl pallet_assets::Config for Test { type MetadataDepositPerByte = ConstU128<0>; type ApprovalDeposit = ConstU128<0>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = (); diff --git a/substrate/frame/assets-freezer/src/impls.rs b/substrate/frame/assets-freezer/src/impls.rs index cd383f1c3cd1..b1a203297cf4 100644 --- a/substrate/frame/assets-freezer/src/impls.rs +++ b/substrate/frame/assets-freezer/src/impls.rs @@ -23,6 +23,7 @@ use frame_support::traits::{ }; use pallet_assets::FrozenBalance; use sp_runtime::traits::Zero; +use storage::StorageDoubleMap; // Implements [`FrozenBalance`] from [`pallet-assets`], so it can understand how much of an // account balance is frozen, and is able to signal to this pallet when to clear the state of an @@ -35,9 +36,22 @@ impl, I: 'static> FrozenBalance::get(asset.clone(), who).is_empty(), + "The list of Freezes should be empty before allowing an account to die" + ); + defensive_assert!( + FrozenBalances::::get(asset.clone(), who).is_none(), + "There should not be a frozen balance before allowing to die" + ); + FrozenBalances::::remove(asset.clone(), who); Freezes::::remove(asset, who); } + + fn contains_freezes(asset: T::AssetId) -> bool { + Freezes::::contains_prefix(asset) + } } // Implement [`fungibles::Inspect`](frame_support::traits::fungibles::Inspect) as it is bound by diff --git a/substrate/frame/assets-freezer/src/mock.rs b/substrate/frame/assets-freezer/src/mock.rs index bc903a018f7b..b1ede3685d6a 100644 --- a/substrate/frame/assets-freezer/src/mock.rs +++ b/substrate/frame/assets-freezer/src/mock.rs @@ -106,6 +106,7 @@ impl pallet_assets::Config for Test { type RemoveItemsLimit = ConstU32<10>; type CallbackHandle = (); type Currency = Balances; + type Holder = (); type Freezer = AssetsFreezer; type RuntimeEvent = RuntimeEvent; type WeightInfo = (); diff --git a/substrate/frame/assets-freezer/src/tests.rs b/substrate/frame/assets-freezer/src/tests.rs index 4f2dea79c705..f509395c8165 100644 --- a/substrate/frame/assets-freezer/src/tests.rs +++ b/substrate/frame/assets-freezer/src/tests.rs @@ -75,10 +75,20 @@ mod impl_frozen_balance { }); } + #[test] + #[should_panic = "The list of Freezes should be empty before allowing an account to die"] + fn died_fails_if_freezes_exist() { + new_test_ext(|| { + test_set_freeze(DummyFreezeReason::Governance, 1); + AssetsFreezer::died(ASSET_ID, &WHO); + }); + } + #[test] fn died_works() { new_test_ext(|| { test_set_freeze(DummyFreezeReason::Governance, 1); + test_thaw(DummyFreezeReason::Governance); AssetsFreezer::died(ASSET_ID, &WHO); assert!(FrozenBalances::::get(ASSET_ID, WHO).is_none()); assert!(Freezes::::get(ASSET_ID, WHO).is_empty()); @@ -168,7 +178,7 @@ mod impl_mutate_freeze { Preservation::Preserve, Fortitude::Polite, ), - 89 + 90 ); System::assert_last_event( Event::::Frozen { asset_id: ASSET_ID, who: WHO, amount: 10 }.into(), @@ -186,7 +196,7 @@ mod impl_mutate_freeze { Preservation::Preserve, Fortitude::Polite, ), - 91 + 92 ); System::assert_last_event( Event::::Thawed { asset_id: ASSET_ID, who: WHO, amount: 2 }.into(), @@ -219,7 +229,7 @@ mod impl_mutate_freeze { Preservation::Preserve, Fortitude::Polite, ), - 89 + 90 ); assert_ok!(AssetsFreezer::extend_freeze( ASSET_ID, @@ -237,7 +247,7 @@ mod impl_mutate_freeze { Preservation::Preserve, Fortitude::Polite, ), - 88 + 89 ); }); } @@ -261,7 +271,7 @@ mod impl_mutate_freeze { Preservation::Preserve, Fortitude::Polite, ), - 89 + 90 ); assert_ok!(AssetsFreezer::thaw(ASSET_ID, &DummyFreezeReason::Governance, &WHO)); System::assert_has_event( @@ -295,10 +305,10 @@ mod with_pallet_assets { 20 )); assert_noop!( - Assets::transfer(RuntimeOrigin::signed(WHO), Compact(ASSET_ID), 2, 80), + Assets::transfer(RuntimeOrigin::signed(WHO), Compact(ASSET_ID), 2, 81), pallet_assets::Error::::BalanceLow, ); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(WHO), Compact(ASSET_ID), 2, 79)); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(WHO), Compact(ASSET_ID), 2, 80)); }); } } diff --git a/substrate/frame/assets-holder/Cargo.toml b/substrate/frame/assets-holder/Cargo.toml new file mode 100644 index 000000000000..7e3aed6e17e9 --- /dev/null +++ b/substrate/frame/assets-holder/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "pallet-assets-holder" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license = "MIT-0" +homepage.workspace = true +repository.workspace = true +description = "Provides holding features to `pallet-assets`" + +[lints] +workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +codec = { workspace = true } +log = { workspace = true } +scale-info = { features = ["derive"], workspace = true } +frame-benchmarking = { optional = true, workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +pallet-assets = { workspace = true } +sp-runtime = { workspace = true } + +[dev-dependencies] +sp-io = { workspace = true } +sp-core = { workspace = true } +pallet-balances = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking?/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-assets/std", + "pallet-balances/std", + "scale-info/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-assets/runtime-benchmarks", + "pallet-balances/runtime-benchmarks", + "sp-runtime/runtime-benchmarks", +] +try-runtime = [ + "frame-support/try-runtime", + "frame-system/try-runtime", + "pallet-assets/try-runtime", + "pallet-balances/try-runtime", + "sp-runtime/try-runtime", +] diff --git a/substrate/frame/assets-holder/src/impl_fungibles.rs b/substrate/frame/assets-holder/src/impl_fungibles.rs new file mode 100644 index 000000000000..b286cbb2eb49 --- /dev/null +++ b/substrate/frame/assets-holder/src/impl_fungibles.rs @@ -0,0 +1,290 @@ +// 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::*; + +use frame_support::traits::{ + fungibles::{Dust, Inspect, InspectHold, MutateHold, Unbalanced, UnbalancedHold}, + tokens::{ + DepositConsequence, Fortitude, Precision, Preservation, Provenance, WithdrawConsequence, + }, +}; +use pallet_assets::BalanceOnHold; +use sp_runtime::{ + traits::{CheckedAdd, CheckedSub, Zero}, + ArithmeticError, +}; +use storage::StorageDoubleMap; + +// Implements [`BalanceOnHold`] from [`pallet-assets`], so it can understand whether there's some +// balance on hold for an asset account, and is able to signal to this pallet when to clear the +// state of an account. +impl, I: 'static> BalanceOnHold + for Pallet +{ + fn balance_on_hold(asset: T::AssetId, who: &T::AccountId) -> Option { + BalancesOnHold::::get(asset, who) + } + + fn died(asset: T::AssetId, who: &T::AccountId) { + defensive_assert!( + Holds::::get(asset.clone(), who).is_empty(), + "The list of Holds should be empty before allowing an account to die" + ); + defensive_assert!( + BalancesOnHold::::get(asset.clone(), who).is_none(), + "The should not be a balance on hold before allowing to die" + ); + + Holds::::remove(asset.clone(), who); + BalancesOnHold::::remove(asset, who); + } + + fn contains_holds(asset: T::AssetId) -> bool { + Holds::::contains_prefix(asset) + } +} + +// Implement [`fungibles::Inspect`](frame_support::traits::fungibles::Inspect) as it is bound by +// [`fungibles::InspectHold`](frame_support::traits::fungibles::InspectHold) and +// [`fungibles::MutateHold`](frame_support::traits::fungibles::MutateHold). To do so, we'll +// re-export all of `pallet-assets` implementation of the same trait. +impl, I: 'static> Inspect for Pallet { + type AssetId = T::AssetId; + type Balance = T::Balance; + + fn total_issuance(asset: Self::AssetId) -> Self::Balance { + pallet_assets::Pallet::::total_issuance(asset) + } + + fn minimum_balance(asset: Self::AssetId) -> Self::Balance { + pallet_assets::Pallet::::minimum_balance(asset) + } + + fn total_balance(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { + pallet_assets::Pallet::::total_balance(asset, who) + } + + fn balance(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { + pallet_assets::Pallet::::balance(asset, who) + } + + fn reducible_balance( + asset: Self::AssetId, + who: &T::AccountId, + preservation: Preservation, + force: Fortitude, + ) -> Self::Balance { + pallet_assets::Pallet::::reducible_balance(asset, who, preservation, force) + } + + fn can_deposit( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> DepositConsequence { + pallet_assets::Pallet::::can_deposit(asset, who, amount, provenance) + } + + fn can_withdraw( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> WithdrawConsequence { + pallet_assets::Pallet::::can_withdraw(asset, who, amount) + } + + fn asset_exists(asset: Self::AssetId) -> bool { + pallet_assets::Pallet::::asset_exists(asset) + } +} + +impl, I: 'static> InspectHold for Pallet { + type Reason = T::RuntimeHoldReason; + + fn total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance { + BalancesOnHold::::get(asset, who).unwrap_or_else(Zero::zero) + } + + fn balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &T::AccountId, + ) -> Self::Balance { + Holds::::get(asset, who) + .iter() + .find(|x| &x.id == reason) + .map(|x| x.amount) + .unwrap_or_else(Zero::zero) + } +} + +impl, I: 'static> Unbalanced for Pallet { + fn handle_dust(dust: Dust) { + let Dust(id, balance) = dust; + pallet_assets::Pallet::::handle_dust(Dust(id, balance)); + } + + fn write_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + ) -> Result, DispatchError> { + pallet_assets::Pallet::::write_balance(asset, who, amount) + } + + fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) { + pallet_assets::Pallet::::set_total_issuance(asset, amount) + } + + fn decrease_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + preservation: Preservation, + force: Fortitude, + ) -> Result { + pallet_assets::Pallet::::decrease_balance( + asset, + who, + amount, + precision, + preservation, + force, + ) + } + + fn increase_balance( + asset: Self::AssetId, + who: &T::AccountId, + amount: Self::Balance, + precision: Precision, + ) -> Result { + pallet_assets::Pallet::::increase_balance(asset, who, amount, precision) + } +} + +impl, I: 'static> UnbalancedHold for Pallet { + fn set_balance_on_hold( + asset: Self::AssetId, + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) -> DispatchResult { + let mut holds = Holds::::get(asset.clone(), who); + let amount_on_hold = + BalancesOnHold::::get(asset.clone(), who).unwrap_or_else(Zero::zero); + + let amount_on_hold = if amount.is_zero() { + if let Some(pos) = holds.iter().position(|x| &x.id == reason) { + let item = &mut holds[pos]; + let amount = item.amount; + + holds.swap_remove(pos); + amount_on_hold.checked_sub(&amount).ok_or(ArithmeticError::Underflow)? + } else { + amount_on_hold + } + } else { + let (increase, delta) = if let Some(pos) = holds.iter().position(|x| &x.id == reason) { + let item = &mut holds[pos]; + let (increase, delta) = + (amount > item.amount, item.amount.max(amount) - item.amount.min(amount)); + + item.amount = amount; + if item.amount.is_zero() { + holds.swap_remove(pos); + } + + (increase, delta) + } else { + holds + .try_push(IdAmount { id: *reason, amount }) + .map_err(|_| Error::::TooManyHolds)?; + (true, amount) + }; + + let amount_on_hold = if increase { + amount_on_hold.checked_add(&delta).ok_or(ArithmeticError::Overflow)? + } else { + amount_on_hold.checked_sub(&delta).ok_or(ArithmeticError::Underflow)? + }; + + amount_on_hold + }; + + if !holds.is_empty() { + Holds::::insert(asset.clone(), who, holds); + } else { + Holds::::remove(asset.clone(), who); + } + + if amount_on_hold.is_zero() { + BalancesOnHold::::remove(asset.clone(), who); + } else { + BalancesOnHold::::insert(asset.clone(), who, amount_on_hold); + } + + Ok(()) + } +} + +impl, I: 'static> MutateHold for Pallet { + fn done_hold( + asset_id: Self::AssetId, + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) { + Self::deposit_event(Event::::Held { + asset_id, + who: who.clone(), + reason: *reason, + amount, + }); + } + + fn done_release( + asset_id: Self::AssetId, + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) { + Self::deposit_event(Event::::Released { + asset_id, + who: who.clone(), + reason: *reason, + amount, + }); + } + + fn done_burn_held( + asset_id: Self::AssetId, + reason: &Self::Reason, + who: &T::AccountId, + amount: Self::Balance, + ) { + Self::deposit_event(Event::::Burned { + asset_id, + who: who.clone(), + reason: *reason, + amount, + }); + } +} diff --git a/substrate/frame/assets-holder/src/lib.rs b/substrate/frame/assets-holder/src/lib.rs new file mode 100644 index 000000000000..ac63a252daaf --- /dev/null +++ b/substrate/frame/assets-holder/src/lib.rs @@ -0,0 +1,177 @@ +// 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 Holder Pallet +//! +//! A pallet capable of holding fungibles from `pallet-assets`. This is an extension of +//! `pallet-assets`, wrapping [`fungibles::Inspect`](`frame_support::traits::fungibles::Inspect`). +//! It implements both +//! [`fungibles::hold::Inspect`](frame_support::traits::fungibles::hold::Inspect), +//! [`fungibles::hold::Mutate`](frame_support::traits::fungibles::hold::Mutate), and especially +//! [`fungibles::hold::Unbalanced`](frame_support::traits::fungibles::hold::Unbalanced). The +//! complexity of the operations is `O(1)`. +//! +//! ## Pallet API +//! +//! See the [`pallet`] module for more information about the interfaces this pallet exposes, +//! including its configuration trait, dispatchables, storage items, events and errors. +//! +//! ## Overview +//! +//! This pallet provides the following functionality: +//! +//! - Pallet hooks allowing [`pallet-assets`] to know the balance on hold for an account on a given +//! asset (see [`pallet_assets::BalanceOnHold`]). +//! - An implementation of +//! [`fungibles::hold::Inspect`](frame_support::traits::fungibles::hold::Inspect), +//! [`fungibles::hold::Mutate`](frame_support::traits::fungibles::hold::Mutate) and +//! [`fungibles::hold::Unbalanced`](frame_support::traits::fungibles::hold::Unbalanced), allowing +//! other pallets to manage holds for the `pallet-assets` assets. + +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::{ + pallet_prelude::*, + traits::{tokens::IdAmount, VariantCount, VariantCountOf}, + BoundedVec, +}; +use frame_system::pallet_prelude::BlockNumberFor; + +pub use pallet::*; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +mod impl_fungibles; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::config(with_default)] + pub trait Config: + frame_system::Config + pallet_assets::Config> + { + /// The overarching freeze reason. + #[pallet::no_default_bounds] + type RuntimeHoldReason: Parameter + Member + MaxEncodedLen + Copy + VariantCount; + + /// The overarching event type. + #[pallet::no_default_bounds] + type RuntimeEvent: From> + + IsType<::RuntimeEvent>; + } + + #[pallet::error] + pub enum Error { + /// Number of holds on an account would exceed the count of `RuntimeHoldReason`. + TooManyHolds, + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event, I: 'static = ()> { + /// `who`s balance on hold was increased by `amount`. + Held { + who: T::AccountId, + asset_id: T::AssetId, + reason: T::RuntimeHoldReason, + amount: T::Balance, + }, + /// `who`s balance on hold was decreased by `amount`. + Released { + who: T::AccountId, + asset_id: T::AssetId, + reason: T::RuntimeHoldReason, + amount: T::Balance, + }, + /// `who`s balance on hold was burned by `amount`. + Burned { + who: T::AccountId, + asset_id: T::AssetId, + reason: T::RuntimeHoldReason, + amount: T::Balance, + }, + } + + /// A map that stores holds applied on an account for a given AssetId. + #[pallet::storage] + pub(super) type Holds, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::AssetId, + Blake2_128Concat, + T::AccountId, + BoundedVec< + IdAmount, + VariantCountOf, + >, + ValueQuery, + >; + + /// A map that stores the current total balance on hold for every account on a given AssetId. + #[pallet::storage] + pub(super) type BalancesOnHold, I: 'static = ()> = StorageDoubleMap< + _, + Blake2_128Concat, + T::AssetId, + Blake2_128Concat, + T::AccountId, + T::Balance, + >; + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn try_state(_: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + Self::do_try_state() + } + } +} + +impl, I: 'static> Pallet { + #[cfg(any(test, feature = "try-runtime"))] + fn do_try_state() -> Result<(), sp_runtime::TryRuntimeError> { + use sp_runtime::{ + traits::{CheckedAdd, Zero}, + ArithmeticError, + }; + + for (asset, who, balance_on_hold) in BalancesOnHold::::iter() { + ensure!(balance_on_hold != Zero::zero(), "zero on hold must not be in state"); + + let mut amount_from_holds: T::Balance = Zero::zero(); + for l in Holds::::get(asset.clone(), who.clone()).iter() { + ensure!(l.amount != Zero::zero(), "zero amount is invalid"); + amount_from_holds = + amount_from_holds.checked_add(&l.amount).ok_or(ArithmeticError::Overflow)?; + } + + frame_support::ensure!( + balance_on_hold == amount_from_holds, + "The `BalancesOnHold` amount is not equal to the sum of `Holds` for (`asset`, `who`)" + ); + } + + Ok(()) + } +} diff --git a/substrate/frame/assets-holder/src/mock.rs b/substrate/frame/assets-holder/src/mock.rs new file mode 100644 index 000000000000..8d9ea1f51a3d --- /dev/null +++ b/substrate/frame/assets-holder/src/mock.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. + +//! Tests mock for `pallet-assets-freezer`. + +use crate as pallet_assets_holder; +pub use crate::*; +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{derive_impl, traits::AsEnsureOriginWithArg}; +use scale_info::TypeInfo; +use sp_runtime::BuildStorage; + +pub type AccountId = ::AccountId; +pub type Balance = ::Balance; +pub type AssetId = ::AssetId; +type Block = frame_system::mocking::MockBlock; + +#[frame_support::runtime] +mod runtime { + #[runtime::runtime] + #[runtime::derive( + RuntimeCall, + RuntimeEvent, + RuntimeError, + RuntimeOrigin, + RuntimeTask, + RuntimeHoldReason, + RuntimeFreezeReason + )] + pub struct Test; + + #[runtime::pallet_index(0)] + pub type System = frame_system; + #[runtime::pallet_index(10)] + pub type Balances = pallet_balances; + #[runtime::pallet_index(20)] + pub type Assets = pallet_assets; + #[runtime::pallet_index(21)] + pub type AssetsHolder = pallet_assets_holder; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountData = pallet_balances::AccountData; +} + +#[derive_impl(pallet_balances::config_preludes::TestDefaultConfig as pallet_balances::DefaultConfig)] +impl pallet_balances::Config for Test { + type AccountStore = System; +} + +#[derive_impl(pallet_assets::config_preludes::TestDefaultConfig as pallet_assets::DefaultConfig)] +impl pallet_assets::Config for Test { + // type AssetAccountDeposit = ConstU64<1>; + type CreateOrigin = AsEnsureOriginWithArg>; + type ForceOrigin = frame_system::EnsureRoot; + type Currency = Balances; + type Holder = AssetsHolder; +} + +#[derive( + Decode, Encode, MaxEncodedLen, PartialEq, Eq, Ord, PartialOrd, TypeInfo, Debug, Clone, Copy, +)] +pub enum DummyHoldReason { + Governance, + Staking, + Other, +} + +impl VariantCount for DummyHoldReason { + // Intentionally set below the actual count of variants, to allow testing for `can_freeze` + const VARIANT_COUNT: u32 = 3; +} + +impl Config for Test { + type RuntimeHoldReason = DummyHoldReason; + type RuntimeEvent = RuntimeEvent; +} + +pub fn new_test_ext(execute: impl FnOnce()) -> sp_io::TestExternalities { + let t = RuntimeGenesisConfig { + assets: pallet_assets::GenesisConfig { + assets: vec![(1, 0, true, 1)], + metadata: vec![], + accounts: vec![(1, 1, 100)], + next_asset_id: None, + }, + system: Default::default(), + balances: Default::default(), + } + .build_storage() + .unwrap(); + let mut ext: sp_io::TestExternalities = t.into(); + ext.execute_with(|| { + System::set_block_number(1); + execute(); + frame_support::assert_ok!(AssetsHolder::do_try_state()); + }); + + ext +} diff --git a/substrate/frame/assets-holder/src/tests.rs b/substrate/frame/assets-holder/src/tests.rs new file mode 100644 index 000000000000..433ed664a144 --- /dev/null +++ b/substrate/frame/assets-holder/src/tests.rs @@ -0,0 +1,558 @@ +// 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. + +//! Tests for pallet-assets-holder. + +use crate::mock::*; + +use frame_support::{ + assert_noop, assert_ok, + traits::tokens::fungibles::{Inspect, InspectHold, MutateHold, UnbalancedHold}, +}; +use pallet_assets::BalanceOnHold; + +const WHO: AccountId = 1; +const ASSET_ID: AssetId = 1; + +fn test_hold(id: DummyHoldReason, amount: Balance) { + assert_ok!(AssetsHolder::set_balance_on_hold(ASSET_ID, &id, &WHO, amount)); +} + +fn test_release(id: DummyHoldReason) { + assert_ok!(AssetsHolder::set_balance_on_hold(ASSET_ID, &id, &WHO, 0)); +} + +mod impl_balance_on_hold { + use super::*; + + #[test] + fn balance_on_hold_works() { + new_test_ext(|| { + assert_eq!( + >::balance_on_hold(ASSET_ID, &WHO), + None + ); + test_hold(DummyHoldReason::Governance, 1); + assert_eq!( + >::balance_on_hold(ASSET_ID, &WHO), + Some(1u64) + ); + test_hold(DummyHoldReason::Staking, 3); + assert_eq!( + >::balance_on_hold(ASSET_ID, &WHO), + Some(4u64) + ); + test_hold(DummyHoldReason::Governance, 2); + assert_eq!( + >::balance_on_hold(ASSET_ID, &WHO), + Some(5u64) + ); + // also test releasing works to reduce a balance, and finally releasing everything + // resets to None + test_release(DummyHoldReason::Governance); + assert_eq!( + >::balance_on_hold(ASSET_ID, &WHO), + Some(3u64) + ); + test_release(DummyHoldReason::Staking); + assert_eq!( + >::balance_on_hold(ASSET_ID, &WHO), + None + ); + }); + } + + #[test] + #[should_panic = "The list of Holds should be empty before allowing an account to die"] + fn died_fails_if_holds_exist() { + new_test_ext(|| { + test_hold(DummyHoldReason::Governance, 1); + AssetsHolder::died(ASSET_ID, &WHO); + }); + } + + #[test] + fn died_works() { + new_test_ext(|| { + test_hold(DummyHoldReason::Governance, 1); + test_release(DummyHoldReason::Governance); + AssetsHolder::died(ASSET_ID, &WHO); + assert!(BalancesOnHold::::get(ASSET_ID, WHO).is_none()); + assert!(Holds::::get(ASSET_ID, WHO).is_empty()); + }); + } +} + +mod impl_hold_inspect { + use super::*; + + #[test] + fn total_balance_on_hold_works() { + new_test_ext(|| { + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 0u64); + test_hold(DummyHoldReason::Governance, 1); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 1u64); + test_hold(DummyHoldReason::Staking, 3); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 4u64); + test_hold(DummyHoldReason::Governance, 2); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 5u64); + // also test release to reduce a balance, and finally releasing everything resets to + // 0 + test_release(DummyHoldReason::Governance); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 3u64); + test_release(DummyHoldReason::Staking); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 0u64); + }); + } + + #[test] + fn balance_on_hold_works() { + new_test_ext(|| { + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 0u64 + ); + test_hold(DummyHoldReason::Governance, 1); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 1u64 + ); + test_hold(DummyHoldReason::Staking, 3); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO + ), + 3u64 + ); + test_hold(DummyHoldReason::Staking, 2); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO + ), + 2u64 + ); + // also test release to reduce a balance, and finally releasing everything resets to + // 0 + test_release(DummyHoldReason::Governance); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 0u64 + ); + test_release(DummyHoldReason::Staking); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO + ), + 0u64 + ); + }); + } +} + +mod impl_hold_unbalanced { + use super::*; + + // Note: Tests for `handle_dust`, `write_balance`, `set_total_issuance`, `decrease_balance` + // and `increase_balance` are intentionally left out without testing, since: + // 1. It is expected these methods are tested within `pallet-assets`, and + // 2. There are no valid cases that can be directly asserted using those methods in + // the scope of this pallet. + + #[test] + fn set_balance_on_hold_works() { + new_test_ext(|| { + assert_eq!(Holds::::get(ASSET_ID, WHO).to_vec(), vec![]); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), None); + // Adding balance on hold works + assert_ok!(AssetsHolder::set_balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 1 + )); + assert_eq!( + Holds::::get(ASSET_ID, WHO).to_vec(), + vec![IdAmount { id: DummyHoldReason::Governance, amount: 1 }] + ); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), Some(1)); + // Increasing hold works + assert_ok!(AssetsHolder::set_balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 3 + )); + assert_eq!( + Holds::::get(ASSET_ID, WHO).to_vec(), + vec![IdAmount { id: DummyHoldReason::Governance, amount: 3 }] + ); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), Some(3)); + // Adding new balance on hold works + assert_ok!(AssetsHolder::set_balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO, + 2 + )); + assert_eq!( + Holds::::get(ASSET_ID, WHO).to_vec(), + vec![ + IdAmount { id: DummyHoldReason::Governance, amount: 3 }, + IdAmount { id: DummyHoldReason::Staking, amount: 2 } + ] + ); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), Some(5)); + + // Note: Assertion skipped to meet @gavofyork's suggestion of matching the number of + // variant count with the number of enum's variants. + // // Adding more than max holds fails + // assert_noop!( + // AssetsHolder::set_balance_on_hold(ASSET_ID, &DummyHoldReason::Other, &WHO, 1), + // Error::::TooManyHolds + // ); + + // Decreasing balance on hold works + assert_ok!(AssetsHolder::set_balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO, + 1 + )); + assert_eq!( + Holds::::get(ASSET_ID, WHO).to_vec(), + vec![ + IdAmount { id: DummyHoldReason::Governance, amount: 3 }, + IdAmount { id: DummyHoldReason::Staking, amount: 1 } + ] + ); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), Some(4)); + // Decreasing until removal of balance on hold works + assert_ok!(AssetsHolder::set_balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 0 + )); + assert_eq!( + Holds::::get(ASSET_ID, WHO).to_vec(), + vec![IdAmount { id: DummyHoldReason::Staking, amount: 1 }] + ); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), Some(1)); + // Clearing ol all holds works + assert_ok!(AssetsHolder::set_balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO, + 0 + )); + assert_eq!(Holds::::get(ASSET_ID, WHO).to_vec(), vec![]); + assert_eq!(BalancesOnHold::::get(ASSET_ID, WHO), None); + }); + } +} + +mod impl_hold_mutate { + use super::*; + use frame_support::traits::tokens::{Fortitude, Precision, Preservation}; + use sp_runtime::TokenError; + + #[test] + fn hold_works() { + super::new_test_ext(|| { + // Holding some `amount` would decrease the asset account balance and change the + // reducible balance, while total issuance is preserved. + assert_ok!(AssetsHolder::hold(ASSET_ID, &DummyHoldReason::Governance, &WHO, 10)); + assert_eq!(Assets::balance(ASSET_ID, &WHO), 90); + // Reducible balance is tested once to ensure token balance model is compliant. + assert_eq!( + Assets::reducible_balance( + ASSET_ID, + &WHO, + Preservation::Expendable, + Fortitude::Force + ), + 89 + ); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 10 + ); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 10); + // Holding preserves `total_balance` + assert_eq!(Assets::total_balance(ASSET_ID, &WHO), 100); + // Holding preserves `total_issuance` + assert_eq!(Assets::total_issuance(ASSET_ID), 100); + + // Increasing the amount on hold for the same reason has the same effect as described + // above in `set_balance_on_hold_works`, while total issuance is preserved. + // Consideration: holding for an amount `x` will increase the already amount on hold by + // `x`. + assert_ok!(AssetsHolder::hold(ASSET_ID, &DummyHoldReason::Governance, &WHO, 20)); + assert_eq!(Assets::balance(ASSET_ID, &WHO), 70); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 30 + ); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 30); + assert_eq!(Assets::total_issuance(ASSET_ID), 100); + + // Holding some amount for a different reason has the same effect as described above in + // `set_balance_on_hold_works`, while total issuance is preserved. + assert_ok!(AssetsHolder::hold(ASSET_ID, &DummyHoldReason::Staking, &WHO, 20)); + assert_eq!(Assets::balance(ASSET_ID, &WHO), 50); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Staking, + &WHO + ), + 20 + ); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 50); + assert_eq!(Assets::total_issuance(ASSET_ID), 100); + }); + } + + fn new_test_ext() -> sp_io::TestExternalities { + super::new_test_ext(|| { + assert_ok!(AssetsHolder::hold(ASSET_ID, &DummyHoldReason::Governance, &WHO, 30)); + assert_ok!(AssetsHolder::hold(ASSET_ID, &DummyHoldReason::Staking, &WHO, 20)); + }) + } + + #[test] + fn release_works() { + // Releasing up to some amount will increase the balance by the released + // amount, while preserving total issuance. + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::release( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 20, + Precision::Exact, + )); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 10 + ); + assert_eq!(Assets::balance(ASSET_ID, WHO), 70); + }); + + // Releasing over the max amount on hold with `BestEffort` will increase the + // balance by the previously amount on hold, while preserving total issuance. + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::release( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 31, + Precision::BestEffort, + )); + assert_eq!( + >::balance_on_hold( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO + ), + 0 + ); + assert_eq!(Assets::balance(ASSET_ID, WHO), 80); + }); + + // Releasing over the max amount on hold with `Exact` will fail. + new_test_ext().execute_with(|| { + assert_noop!( + AssetsHolder::release( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 31, + Precision::Exact, + ), + TokenError::FundsUnavailable + ); + }); + } + + #[test] + fn burn_held_works() { + // Burning works, reducing total issuance and `total_balance`. + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::burn_held( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 1, + Precision::BestEffort, + Fortitude::Polite + )); + assert_eq!(Assets::total_balance(ASSET_ID, &WHO), 99); + assert_eq!(Assets::total_issuance(ASSET_ID), 99); + }); + + // Burning by an amount up to the balance on hold with `Exact` works, reducing balance on + // hold up to the given amount. + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::burn_held( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 10, + Precision::Exact, + Fortitude::Polite + )); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 40); + assert_eq!(Assets::balance(ASSET_ID, WHO), 50); + }); + + // Burning by an amount over the balance on hold with `BestEffort` works, reducing balance + // on hold up to the given amount. + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::burn_held( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 31, + Precision::BestEffort, + Fortitude::Polite + )); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 20); + assert_eq!(Assets::balance(ASSET_ID, WHO), 50); + }); + + // Burning by an amount over the balance on hold with `Exact` fails. + new_test_ext().execute_with(|| { + assert_noop!( + AssetsHolder::burn_held( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 31, + Precision::Exact, + Fortitude::Polite + ), + TokenError::FundsUnavailable + ); + }); + } + + #[test] + fn burn_all_held_works() { + new_test_ext().execute_with(|| { + // Burning all balance on hold works as burning passing it as amount with `BestEffort` + assert_ok!(AssetsHolder::burn_all_held( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + Precision::BestEffort, + Fortitude::Polite, + )); + assert_eq!(AssetsHolder::total_balance_on_hold(ASSET_ID, &WHO), 20); + assert_eq!(Assets::balance(ASSET_ID, WHO), 50); + }); + } + + #[test] + fn done_held_works() { + new_test_ext().execute_with(|| { + System::assert_has_event( + Event::::Held { + who: WHO, + asset_id: ASSET_ID, + reason: DummyHoldReason::Governance, + amount: 30, + } + .into(), + ); + }); + } + + #[test] + fn done_release_works() { + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::release( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + 31, + Precision::BestEffort + )); + System::assert_has_event( + Event::::Released { + who: WHO, + asset_id: ASSET_ID, + reason: DummyHoldReason::Governance, + amount: 30, + } + .into(), + ); + }); + } + + #[test] + fn done_burn_held_works() { + new_test_ext().execute_with(|| { + assert_ok!(AssetsHolder::burn_all_held( + ASSET_ID, + &DummyHoldReason::Governance, + &WHO, + Precision::BestEffort, + Fortitude::Polite, + )); + System::assert_has_event( + Event::::Burned { + who: WHO, + asset_id: ASSET_ID, + reason: DummyHoldReason::Governance, + amount: 30, + } + .into(), + ); + }); + } +} diff --git a/substrate/frame/assets/src/functions.rs b/substrate/frame/assets/src/functions.rs index c218c4ddc952..2285d4a254b5 100644 --- a/substrate/frame/assets/src/functions.rs +++ b/substrate/frame/assets/src/functions.rs @@ -27,6 +27,7 @@ pub(super) enum DeadConsequence { Keep, } +use frame_support::storage::with_storage_layer; use DeadConsequence::*; // The main implementation block for the module. @@ -96,12 +97,20 @@ impl, I: 'static> Pallet { } pub(super) fn dead_account( + id: T::AssetId, who: &T::AccountId, d: &mut AssetDetails>, reason: &ExistenceReasonOf, force: bool, - ) -> DeadConsequence { + ) -> Result { use ExistenceReason::*; + + ensure!( + T::Holder::balance_on_hold(id.clone(), &who).is_none(), + Error::::ContainsHolds + ); + ensure!(T::Freezer::frozen_balance(id, &who).is_none(), Error::::ContainsFreezes); + match *reason { Consumer => frame_system::Pallet::::dec_consumers(who), Sufficient => { @@ -109,11 +118,11 @@ impl, I: 'static> Pallet { frame_system::Pallet::::dec_sufficients(who); }, DepositRefunded => {}, - DepositHeld(_) | DepositFrom(..) if !force => return Keep, + DepositHeld(_) | DepositFrom(..) if !force => return Ok(Keep), DepositHeld(_) | DepositFrom(..) => {}, } d.accounts = d.accounts.saturating_sub(1); - Remove + Ok(Remove) } /// Returns `true` when the balance of `account` can be increased by `amount`. @@ -193,22 +202,37 @@ impl, I: 'static> Pallet { return Frozen } if let Some(rest) = account.balance.checked_sub(&amount) { - if let Some(frozen) = T::Freezer::frozen_balance(id.clone(), who) { - match frozen.checked_add(&details.min_balance) { - Some(required) if rest < required => return Frozen, - None => return Overflow, - _ => {}, - } - } - - if rest < details.min_balance { - if keep_alive { - WouldDie - } else { - ReducedToZero(rest) - } - } else { - Success + match ( + T::Holder::balance_on_hold(id.clone(), who), + T::Freezer::frozen_balance(id.clone(), who), + ) { + (None, None) => + if rest < details.min_balance { + if keep_alive { + WouldDie + } else { + ReducedToZero(rest) + } + } else { + Success + }, + (maybe_held, maybe_frozen) => { + let frozen = maybe_frozen.unwrap_or_default(); + let held = maybe_held.unwrap_or_default(); + + // The `untouchable` balance of the asset account of `who`. This is described + // here: https://paritytech.github.io/polkadot-sdk/master/frame_support/traits/tokens/fungible/index.html#visualising-balance-components-together- + let untouchable = frozen.saturating_sub(held).max(details.min_balance); + if rest < untouchable { + if !frozen.is_zero() { + Frozen + } else { + WouldDie + } + } else { + Success + } + }, } } else { BalanceLow @@ -228,20 +252,21 @@ impl, I: 'static> Pallet { let account = Account::::get(&id, who).ok_or(Error::::NoAccount)?; ensure!(!account.status.is_frozen(), Error::::Frozen); - let amount = if let Some(frozen) = T::Freezer::frozen_balance(id, who) { - // Frozen balance: account CANNOT be deleted - let required = - frozen.checked_add(&details.min_balance).ok_or(ArithmeticError::Overflow)?; - account.balance.saturating_sub(required) - } else { - if keep_alive { - // We want to keep the account around. - account.balance.saturating_sub(details.min_balance) - } else { - // Don't care if the account dies - account.balance - } + let untouchable = match ( + T::Holder::balance_on_hold(id.clone(), who), + T::Freezer::frozen_balance(id.clone(), who), + keep_alive, + ) { + (None, None, true) => details.min_balance, + (None, None, false) => Zero::zero(), + (maybe_held, maybe_frozen, _) => { + let held = maybe_held.unwrap_or_default(); + let frozen = maybe_frozen.unwrap_or_default(); + frozen.saturating_sub(held).max(details.min_balance) + }, }; + let amount = account.balance.saturating_sub(untouchable); + Ok(amount.min(details.supply)) } @@ -351,28 +376,35 @@ impl, I: 'static> Pallet { pub(super) fn do_refund(id: T::AssetId, who: T::AccountId, allow_burn: bool) -> DispatchResult { use AssetStatus::*; use ExistenceReason::*; + let mut account = Account::::get(&id, &who).ok_or(Error::::NoDeposit)?; ensure!(matches!(account.reason, Consumer | DepositHeld(..)), Error::::NoDeposit); let mut details = Asset::::get(&id).ok_or(Error::::Unknown)?; ensure!(matches!(details.status, Live | Frozen), Error::::IncorrectStatus); ensure!(account.balance.is_zero() || allow_burn, Error::::WouldBurn); - if let Some(deposit) = account.reason.take_deposit() { - T::Currency::unreserve(&who, deposit); - } + with_storage_layer(|| { + if let Some(deposit) = account.reason.take_deposit() { + T::Currency::unreserve(&who, deposit); + } - if let Remove = Self::dead_account(&who, &mut details, &account.reason, false) { - Account::::remove(&id, &who); - } else { - debug_assert!(false, "refund did not result in dead account?!"); - // deposit may have been refunded, need to update `Account` - Account::::insert(id, &who, account); - return Ok(()) - } - Asset::::insert(&id, details); - // Executing a hook here is safe, since it is not in a `mutate`. - T::Freezer::died(id, &who); - Ok(()) + if let Remove = + Self::dead_account(id.clone(), &who, &mut details, &account.reason, false)? + { + Account::::remove(&id, &who); + } else { + debug_assert!(false, "refund did not result in dead account?!"); + // deposit may have been refunded, need to update `Account` + Account::::insert(id, &who, account); + return Ok(()) + } + + Asset::::insert(&id, details); + // Executing a hook here is safe, since it is not in a `mutate`. + T::Freezer::died(id.clone(), &who); + T::Holder::died(id, &who); + Ok(()) + }) } /// Refunds the `DepositFrom` of an account only if its balance is zero. @@ -395,20 +427,25 @@ impl, I: 'static> Pallet { } ensure!(account.balance.is_zero(), Error::::WouldBurn); - T::Currency::unreserve(&depositor, deposit); + with_storage_layer(|| { + T::Currency::unreserve(&depositor, deposit); - if let Remove = Self::dead_account(&who, &mut details, &account.reason, false) { - Account::::remove(&id, &who); - } else { - debug_assert!(false, "refund did not result in dead account?!"); - // deposit may have been refunded, need to update `Account` - Account::::insert(&id, &who, account); + if let Remove = + Self::dead_account(id.clone(), &who, &mut details, &account.reason, false)? + { + Account::::remove(&id, &who); + } else { + debug_assert!(false, "refund did not result in dead account?!"); + // deposit may have been refunded, need to update `Account` + Account::::insert(&id, &who, account); + return Ok(()) + } + Asset::::insert(&id, details); + // Executing a hook here is safe, since it is not in a `mutate`. + T::Freezer::died(id.clone(), &who); + T::Holder::died(id, &who); return Ok(()) - } - Asset::::insert(&id, details); - // Executing a hook here is safe, since it is not in a `mutate`. - T::Freezer::died(id, &who); - return Ok(()) + }) } /// Increases the asset `id` balance of `beneficiary` by `amount`. @@ -561,7 +598,13 @@ impl, I: 'static> Pallet { account.balance = account.balance.saturating_sub(actual); if account.balance < details.min_balance { debug_assert!(account.balance.is_zero(), "checked in prep; qed"); - target_died = Some(Self::dead_account(target, details, &account.reason, false)); + target_died = Some(Self::dead_account( + id.clone(), + target, + details, + &account.reason, + false, + )?); if let Some(Remove) = target_died { return Ok(()) } @@ -575,7 +618,8 @@ impl, I: 'static> Pallet { // Execute hook outside of `mutate`. if let Some(Remove) = target_died { - T::Freezer::died(id, target); + T::Freezer::died(id.clone(), target); + T::Holder::died(id, target); } Ok(actual) } @@ -599,7 +643,8 @@ impl, I: 'static> Pallet { let (balance, died) = Self::transfer_and_die(id.clone(), source, dest, amount, maybe_need_admin, f)?; if let Some(Remove) = died { - T::Freezer::died(id, source); + T::Freezer::died(id.clone(), source); + T::Holder::died(id, source); } Ok(balance) } @@ -654,41 +699,48 @@ impl, I: 'static> Pallet { debug_assert!(source_account.balance >= debit, "checked in prep; qed"); source_account.balance = source_account.balance.saturating_sub(debit); - Account::::try_mutate(&id, &dest, |maybe_account| -> DispatchResult { - match maybe_account { - Some(ref mut account) => { - // Calculate new balance; this will not saturate since it's already checked - // in prep. - debug_assert!( - account.balance.checked_add(&credit).is_some(), - "checked in prep; qed" - ); - account.balance.saturating_accrue(credit); - }, - maybe_account @ None => { - *maybe_account = Some(AssetAccountOf:: { - balance: credit, - status: AccountStatus::Liquid, - reason: Self::new_account(dest, details, None)?, - extra: T::Extra::default(), - }); - }, + with_storage_layer(|| { + Account::::try_mutate(&id, &dest, |maybe_account| -> DispatchResult { + match maybe_account { + Some(ref mut account) => { + // Calculate new balance; this will not saturate since it's already + // checked in prep. + debug_assert!( + account.balance.checked_add(&credit).is_some(), + "checked in prep; qed" + ); + account.balance.saturating_accrue(credit); + }, + maybe_account @ None => { + *maybe_account = Some(AssetAccountOf:: { + balance: credit, + status: AccountStatus::Liquid, + reason: Self::new_account(dest, details, None)?, + extra: T::Extra::default(), + }); + }, + } + Ok(()) + })?; + + // Remove source account if it's now dead. + if source_account.balance < details.min_balance { + debug_assert!(source_account.balance.is_zero(), "checked in prep; qed"); + source_died = Some(Self::dead_account( + id.clone(), + source, + details, + &source_account.reason, + false, + )?); + if let Some(Remove) = source_died { + Account::::remove(&id, &source); + return Ok(()) + } } + Account::::insert(&id, &source, &source_account); Ok(()) - })?; - - // Remove source account if it's now dead. - if source_account.balance < details.min_balance { - debug_assert!(source_account.balance.is_zero(), "checked in prep; qed"); - source_died = - Some(Self::dead_account(source, details, &source_account.reason, false)); - if let Some(Remove) = source_died { - Account::::remove(&id, &source); - return Ok(()) - } - } - Account::::insert(&id, &source, &source_account); - Ok(()) + }) })?; Self::deposit_event(Event::Transferred { @@ -753,6 +805,10 @@ impl, I: 'static> Pallet { if let Some(check_owner) = maybe_check_owner { ensure!(details.owner == check_owner, Error::::NoPermission); } + + ensure!(!T::Holder::contains_holds(id.clone()), Error::::ContainsHolds); + ensure!(!T::Freezer::contains_freezes(id.clone()), Error::::ContainsFreezes); + details.status = AssetStatus::Destroying; Self::deposit_event(Event::DestructionStarted { asset_id: id }); @@ -775,31 +831,36 @@ impl, I: 'static> Pallet { let mut details = maybe_details.as_mut().ok_or(Error::::Unknown)?; // Should only destroy accounts while the asset is in a destroying state ensure!(details.status == AssetStatus::Destroying, Error::::IncorrectStatus); - for (i, (who, mut v)) in Account::::iter_prefix(&id).enumerate() { - // unreserve the existence deposit if any - if let Some((depositor, deposit)) = v.reason.take_deposit_from() { - T::Currency::unreserve(&depositor, deposit); - } else if let Some(deposit) = v.reason.take_deposit() { - T::Currency::unreserve(&who, deposit); - } - if let Remove = Self::dead_account(&who, &mut details, &v.reason, false) { - Account::::remove(&id, &who); - dead_accounts.push(who); - } else { - // deposit may have been released, need to update `Account` - Account::::insert(&id, &who, v); - defensive!("destroy did not result in dead account?!"); - } - if i + 1 >= (max_items as usize) { - break + with_storage_layer(|| { + for (i, (who, mut v)) in Account::::iter_prefix(&id).enumerate() { + // unreserve the existence deposit if any + if let Some((depositor, deposit)) = v.reason.take_deposit_from() { + T::Currency::unreserve(&depositor, deposit); + } else if let Some(deposit) = v.reason.take_deposit() { + T::Currency::unreserve(&who, deposit); + } + if let Remove = + Self::dead_account(id.clone(), &who, &mut details, &v.reason, false)? + { + Account::::remove(&id, &who); + dead_accounts.push(who); + } else { + // deposit may have been released, need to update `Account` + Account::::insert(&id, &who, v); + defensive!("destroy did not result in dead account?!"); + } + if i + 1 >= (max_items as usize) { + break + } } - } - remaining_accounts = details.accounts; - Ok(()) + remaining_accounts = details.accounts; + Ok(()) + }) })?; for who in &dead_accounts { T::Freezer::died(id.clone(), &who); + T::Holder::died(id.clone(), &who); } Self::deposit_event(Event::AccountsDestroyed { @@ -960,7 +1021,8 @@ impl, I: 'static> Pallet { // Execute hook outside of `mutate`. if let Some(Remove) = owner_died { - T::Freezer::died(id, owner); + T::Freezer::died(id.clone(), owner); + T::Holder::died(id, owner); } Ok(()) } diff --git a/substrate/frame/assets/src/impl_fungibles.rs b/substrate/frame/assets/src/impl_fungibles.rs index 578fa08c4e63..6ab7e941ea1a 100644 --- a/substrate/frame/assets/src/impl_fungibles.rs +++ b/substrate/frame/assets/src/impl_fungibles.rs @@ -47,7 +47,8 @@ impl, I: 'static> fungibles::Inspect<::AccountId } fn total_balance(asset: Self::AssetId, who: &::AccountId) -> Self::Balance { - Pallet::::balance(asset, who) + Pallet::::balance(asset.clone(), who) + .saturating_add(T::Holder::balance_on_hold(asset, who).unwrap_or_default()) } fn reducible_balance( diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index e909932bfc82..e74a68963ab3 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -295,6 +295,8 @@ pub mod pallet { type MetadataDepositPerByte = ConstU64<1>; type ApprovalDeposit = ConstU64<1>; type StringLimit = ConstU32<50>; + type Freezer = (); + type Holder = (); type Extra = (); type CallbackHandle = (); type WeightInfo = (); @@ -390,9 +392,12 @@ pub mod pallet { /// A hook to allow a per-asset, per-account minimum balance to be enforced. This must be /// respected in all permissionless operations. - #[pallet::no_default] type Freezer: FrozenBalance; + /// A hook to inspect a per-asset, per-account balance that is held. This goes in + /// accordance with balance model. + type Holder: BalanceOnHold; + /// Additional data to be stored with an account's asset balance. type Extra: Member + Parameter + Default + MaxEncodedLen; @@ -688,6 +693,10 @@ pub mod pallet { CallbackFailed, /// The asset ID must be equal to the [`NextAssetId`]. BadAssetId, + /// The asset cannot be destroyed because some accounts for this asset contain freezes. + ContainsFreezes, + /// The asset cannot be destroyed because some accounts for this asset contain holds. + ContainsHolds, } #[pallet::call(weight(>::WeightInfo))] @@ -801,6 +810,9 @@ pub mod pallet { /// /// - `id`: The identifier of the asset to be destroyed. This must identify an existing /// asset. + /// + /// It will fail with either [`Error::ContainsHolds`] or [`Error::ContainsFreezes`] if + /// an account contains holds or freezes in place. #[pallet::call_index(2)] pub fn start_destroy(origin: OriginFor, id: T::AssetIdParameter) -> DispatchResult { let maybe_check_owner = match T::ForceOrigin::try_origin(origin) { @@ -1615,6 +1627,9 @@ pub mod pallet { /// refunded. /// - `allow_burn`: If `true` then assets may be destroyed in order to complete the refund. /// + /// It will fail with either [`Error::ContainsHolds`] or [`Error::ContainsFreezes`] if + /// the asset account contains holds or freezes in place. + /// /// Emits `Refunded` event when successful. #[pallet::call_index(27)] #[pallet::weight(T::WeightInfo::refund())] @@ -1705,6 +1720,9 @@ pub mod pallet { /// - `id`: The identifier of the asset for the account holding a deposit. /// - `who`: The account to refund. /// + /// It will fail with either [`Error::ContainsHolds`] or [`Error::ContainsFreezes`] if + /// the asset account contains holds or freezes in place. + /// /// Emits `Refunded` event when successful. #[pallet::call_index(30)] #[pallet::weight(T::WeightInfo::refund_other())] diff --git a/substrate/frame/assets/src/mock.rs b/substrate/frame/assets/src/mock.rs index 2c160840e147..9803f929a566 100644 --- a/substrate/frame/assets/src/mock.rs +++ b/substrate/frame/assets/src/mock.rs @@ -22,7 +22,7 @@ use crate as pallet_assets; use codec::Encode; use frame_support::{ - construct_runtime, derive_impl, parameter_types, + assert_ok, construct_runtime, derive_impl, parameter_types, traits::{AsEnsureOriginWithArg, ConstU32}, }; use sp_io::storage; @@ -103,6 +103,7 @@ impl Config for Test { type CreateOrigin = AsEnsureOriginWithArg>; type ForceOrigin = frame_system::EnsureRoot; type Freezer = TestFreezer; + type Holder = TestHolder; type CallbackHandle = (AssetsCallbackHandle, AutoIncAssetId); } @@ -114,9 +115,50 @@ pub enum Hook { } parameter_types! { static Frozen: HashMap<(u32, u64), u64> = Default::default(); + static OnHold: HashMap<(u32, u64), u64> = Default::default(); static Hooks: Vec = Default::default(); } +pub struct TestHolder; +impl BalanceOnHold for TestHolder { + fn balance_on_hold(asset: u32, who: &u64) -> Option { + OnHold::get().get(&(asset, *who)).cloned() + } + + fn died(asset: u32, who: &u64) { + Hooks::mutate(|v| v.push(Hook::Died(asset, *who))) + } + + fn contains_holds(asset: AssetId) -> bool { + OnHold::get().iter().any(|((k, _), _)| &asset == k) + } +} + +pub(crate) fn set_balance_on_hold(asset: u32, who: u64, amount: u64) { + OnHold::mutate(|v| { + let amount_on_hold = v.get(&(asset, who)).unwrap_or(&0); + + if &amount > amount_on_hold { + // Hold more funds + let amount = amount - amount_on_hold; + let f = DebitFlags { keep_alive: true, best_effort: false }; + assert_ok!(Assets::decrease_balance(asset, &who, amount, f, |_, _| Ok(()))); + } else { + // Release funds on hold + let amount = amount_on_hold - amount; + assert_ok!(Assets::increase_balance(asset, &who, amount, |_| Ok(()))); + } + + // Asset amount still "exists", we just store it here + v.insert((asset, who), amount); + }); +} + +pub(crate) fn clear_balance_on_hold(asset: u32, who: u64) { + OnHold::mutate(|v| { + v.remove(&(asset, who)); + }); +} pub struct TestFreezer; impl FrozenBalance for TestFreezer { fn frozen_balance(asset: u32, who: &u64) -> Option { @@ -129,6 +171,11 @@ impl FrozenBalance for TestFreezer { // Sanity check: dead accounts have no balance. assert!(Assets::balance(asset, *who).is_zero()); } + + /// Return a value that indicates if there are registered freezes for a given asset. + fn contains_freezes(asset: AssetId) -> bool { + Frozen::get().iter().any(|((k, _), _)| &asset == k) + } } pub(crate) fn set_frozen_balance(asset: u32, who: u64, amount: u64) { diff --git a/substrate/frame/assets/src/tests.rs b/substrate/frame/assets/src/tests.rs index 75a6139702c6..0b6f55a9af82 100644 --- a/substrate/frame/assets/src/tests.rs +++ b/substrate/frame/assets/src/tests.rs @@ -209,7 +209,15 @@ fn refunding_calls_died_hook() { assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, true)); assert_eq!(Asset::::get(0).unwrap().accounts, 0); - assert_eq!(hooks(), vec![Hook::Died(0, 1)]); + assert_eq!( + hooks(), + vec![ + Hook::Died(0, 1), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 1) + ] + ); assert_eq!(asset_ids(), vec![0, 999]); }); } @@ -650,27 +658,59 @@ fn min_balance_should_work() { assert!(Assets::maybe_balance(0, 1).is_none()); assert_eq!(Assets::balance(0, 2), 100); assert_eq!(Asset::::get(0).unwrap().accounts, 1); - assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); + assert_eq!( + take_hooks(), + vec![ + Hook::Died(0, 1), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 1) + ] + ); // Death by `force_transfer`. assert_ok!(Assets::force_transfer(RuntimeOrigin::signed(1), 0, 2, 1, 91)); assert!(Assets::maybe_balance(0, 2).is_none()); assert_eq!(Assets::balance(0, 1), 100); assert_eq!(Asset::::get(0).unwrap().accounts, 1); - assert_eq!(take_hooks(), vec![Hook::Died(0, 2)]); + assert_eq!( + take_hooks(), + vec![ + Hook::Died(0, 2), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 2) + ] + ); // Death by `burn`. assert_ok!(Assets::burn(RuntimeOrigin::signed(1), 0, 1, 91)); assert!(Assets::maybe_balance(0, 1).is_none()); assert_eq!(Asset::::get(0).unwrap().accounts, 0); - assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); + assert_eq!( + take_hooks(), + vec![ + Hook::Died(0, 1), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 1) + ] + ); // Death by `transfer_approved`. assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); Balances::make_free_balance_be(&1, 2); assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 100)); assert_ok!(Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 3, 91)); - assert_eq!(take_hooks(), vec![Hook::Died(0, 1)]); + assert_eq!( + take_hooks(), + vec![ + Hook::Died(0, 1), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 1) + ] + ); }); } @@ -837,8 +877,8 @@ fn transfer_all_works_3() { assert_ok!(Assets::mint(RuntimeOrigin::signed(0), 0, 2, 100)); // transfer all and allow death w/ frozen assert_ok!(Assets::transfer_all(Some(1).into(), 0, 2, false)); - assert_eq!(Assets::balance(0, &1), 110); - assert_eq!(Assets::balance(0, &2), 200); + assert_eq!(Assets::balance(0, &1), 100); + assert_eq!(Assets::balance(0, &2), 210); }); } @@ -1302,6 +1342,130 @@ fn set_metadata_should_work() { }); } +/// Calling on `dead_account` should be either unreachable, or fail if either a freeze or some +/// balance on hold exists. +/// +/// ### Case 1: Sufficient asset +/// +/// This asserts for `dead_account` on `decrease_balance`, `transfer_and_die` and +/// `do_destry_accounts`. +#[test] +fn calling_dead_account_fails_if_freezes_or_balances_on_hold_exist_1() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 50)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + + set_frozen_balance(0, 1, 50); + // Cannot transfer out less than max(freezes, ed). This happens in + // `prep_debit` under `transfer_and_die`. Would not reach `dead_account`. + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100), + Error::::BalanceLow + ); + assert_noop!( + Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 100), + Error::::BalanceLow + ); + assert_noop!( + Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 100), + Error::::BalanceLow + ); + // Cannot start destroying the asset, because some accounts contain freezes + assert_noop!( + Assets::start_destroy(RuntimeOrigin::signed(1), 0), + Error::::ContainsFreezes + ); + clear_frozen_balance(0, 1); + + set_balance_on_hold(0, 1, 50); + // Cannot transfer out less than max(freezes, ed). This happens in + // `prep_debit` under `transfer_and_die`. Would not reach `dead_account`. + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 100), + Error::::BalanceLow + ); + assert_noop!( + Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 100), + Error::::BalanceLow + ); + assert_noop!( + Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 100), + Error::::BalanceLow + ); + // Cannot start destroying the asset, because some accounts contain freezes + assert_noop!( + Assets::start_destroy(RuntimeOrigin::signed(1), 0), + Error::::ContainsHolds + ); + }) +} + +/// Calling on `dead_account` should be either unreachable, or fail if either a freeze or some +/// balance on hold exists. +/// +/// ### Case 2: Inufficient asset +/// +/// This asserts for `dead_account` on `do_refund` and `do_refund_other`. +#[test] +fn calling_dead_account_fails_if_freezes_or_balances_on_hold_exist_2() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch(RuntimeOrigin::signed(1), 0)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + + set_frozen_balance(0, 1, 50); + + let mut account = + Account::::get(&0, &1).expect("account has already been touched; qed"); + let touch_deposit = + account.reason.take_deposit().expect("account was created by touching it; qed"); + + assert_noop!( + Assets::refund(RuntimeOrigin::signed(1), 0, true), + Error::::ContainsFreezes + ); + + // Assert touch deposit is not tainted. + let deposit_after_noop = + Account::::get(&0, &1).and_then(|mut account| account.reason.take_deposit()); + assert_eq!(deposit_after_noop, Some(touch_deposit)); + + clear_frozen_balance(0, 1); + + set_balance_on_hold(0, 1, 50); + assert_noop!( + Assets::refund(RuntimeOrigin::signed(1), 0, true), + Error::::ContainsHolds + ); + clear_balance_on_hold(0, 1); + assert_ok!(Assets::refund(RuntimeOrigin::signed(1), 0, true)); + }); + + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, false, 1)); + Balances::make_free_balance_be(&1, 100); + assert_ok!(Assets::touch_other(RuntimeOrigin::signed(1), 0, 2)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 2, 100)); + + set_frozen_balance(0, 2, 100); + assert_noop!( + Assets::refund_other(RuntimeOrigin::signed(1), 0, 2), + Error::::WouldBurn + ); + clear_frozen_balance(0, 2); + + // Note: It's not possible to set balance on hold for the maximum balance, + // as it `WouldBurn` because of how setting the balance works on mock. + set_balance_on_hold(0, 2, 99); + assert_noop!( + Assets::refund_other(RuntimeOrigin::signed(1), 0, 2), + Error::::WouldBurn + ); + clear_balance_on_hold(0, 2); + }) +} + /// Destroying an asset calls the `FrozenBalance::died` hooks of all accounts. #[test] fn destroy_accounts_calls_died_hooks() { @@ -1316,7 +1480,19 @@ fn destroy_accounts_calls_died_hooks() { assert_ok!(Assets::destroy_accounts(RuntimeOrigin::signed(1), 0)); // Accounts 1 and 2 died. - assert_eq!(hooks(), vec![Hook::Died(0, 1), Hook::Died(0, 2)]); + assert_eq!( + hooks(), + vec![ + Hook::Died(0, 1), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 1), + Hook::Died(0, 2), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 2) + ] + ); }) } @@ -1345,9 +1521,15 @@ fn freezer_should_work() { // freeze 50 of it. set_frozen_balance(0, 1, 50); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 20)); - // cannot transfer another 21 away as this would take the non-frozen balance (30) to below - // the minimum balance (10). + // Note: The amount to be transferred in this step changed deliberately from 20 to 30 + // (https://github.com/paritytech/polkadot-sdk/pull/4530/commits/2ab35354d86904c035b21a2229452841b79b0457) + // to reflect the change in how `reducible_balance` is calculated: from untouchable = ed + + // frozen, to untouchalbe = max(ed, frozen) + // + // This is done in this line so most of the remaining test is preserved without changes + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 30)); + // cannot transfer another 21 away as this would take the spendable balance (30) to below + // zero. assert_noop!( Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 21), Error::::BalanceLow @@ -1370,8 +1552,60 @@ fn freezer_should_work() { // and if we clear it, we can remove the account completely. clear_frozen_balance(0, 1); - assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 50)); - assert_eq!(hooks(), vec![Hook::Died(0, 1)]); + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 49)); + assert_eq!( + hooks(), + vec![ + Hook::Died(0, 1), + // Note: Hooks get called twice because the hook is called from `Holder` AND + // `Freezer`. + Hook::Died(0, 1) + ] + ); + }); +} + +#[test] +fn freezing_and_holds_work() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 10)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + assert_eq!(Assets::balance(0, 1), 100); + + // Hold 50 of it + set_balance_on_hold(0, 1, 50); + assert_eq!(Assets::balance(0, 1), 50); + assert_eq!(TestHolder::balance_on_hold(0, &1), Some(50)); + + // Can freeze up to held + min_balance without affecting reducible + set_frozen_balance(0, 1, 59); + assert_eq!(Assets::reducible_balance(0, &1, true), Ok(40)); + set_frozen_balance(0, 1, 61); + assert_eq!(Assets::reducible_balance(0, &1, true), Ok(39)); + + // Increasing hold is not necessarily restricted by the frozen balance + set_balance_on_hold(0, 1, 62); + assert_eq!(Assets::reducible_balance(0, &1, true), Ok(28)); + + // Transfers are bound to the spendable amount + assert_noop!( + Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 29), + Error::::BalanceLow + ); + // Approved transfers fail as well + Balances::make_free_balance_be(&1, 2); + assert_ok!(Assets::approve_transfer(RuntimeOrigin::signed(1), 0, 2, 29)); + assert_noop!( + Assets::transfer_approved(RuntimeOrigin::signed(2), 0, 1, 2, 29), + Error::::BalanceLow + ); + // Also forced transfers fail + assert_noop!( + Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 29), + Error::::BalanceLow + ); + // ...but transferring up to spendable works + assert_ok!(Assets::transfer(RuntimeOrigin::signed(1), 0, 2, 28)); }); } @@ -1735,6 +1969,31 @@ fn root_asset_create_should_work() { }); } +#[test] +fn asset_start_destroy_fails_if_there_are_holds_or_freezes() { + new_test_ext().execute_with(|| { + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 1, true, 1)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(1), 0, 1, 100)); + + set_frozen_balance(0, 1, 50); + assert_noop!( + Assets::start_destroy(RuntimeOrigin::signed(1), 0), + Error::::ContainsFreezes + ); + + set_balance_on_hold(0, 1, 50); + assert_noop!( + Assets::start_destroy(RuntimeOrigin::signed(1), 0), + Error::::ContainsHolds + ); + + clear_frozen_balance(0, 1); + clear_balance_on_hold(0, 1); + + assert_ok!(Assets::start_destroy(RuntimeOrigin::signed(1), 0)); + }); +} + #[test] fn asset_create_and_destroy_is_reverted_if_callback_fails() { new_test_ext().execute_with(|| { diff --git a/substrate/frame/assets/src/types.rs b/substrate/frame/assets/src/types.rs index 11edc7d3fcb5..3f1b6aeaf072 100644 --- a/substrate/frame/assets/src/types.rs +++ b/substrate/frame/assets/src/types.rs @@ -174,7 +174,13 @@ impl AccountStatus { #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] pub struct AssetAccount { - /// The balance. + /// The `free` balance of an asset account, which can be potentially used for an account, if + /// certain constraints are met. + /// + /// This balance is not the same as the _reducible_/_spendable_ balance of the asset account + /// (calculated on [`Pallet::reducible_balance`], and depends on freezes, defined on + /// [`Config::Freezer`], if any), and it's not the same as the _balance on hold_ (which depends + /// on [`Config::Holder`], if any). pub(super) balance: Balance, /// The status of the account. pub(super) status: AccountStatus, @@ -220,9 +226,10 @@ pub trait FrozenBalance { fn frozen_balance(asset: AssetId, who: &AccountId) -> Option; /// Called after an account has been removed. - /// - /// NOTE: It is possible that the asset does no longer exist when this hook is called. fn died(asset: AssetId, who: &AccountId); + + /// Return a value that indicates if there are registered freezes for a given asset. + fn contains_freezes(asset: AssetId) -> bool; } impl FrozenBalance for () { @@ -230,6 +237,44 @@ impl FrozenBalance for None } fn died(_: AssetId, _: &AccountId) {} + fn contains_freezes(_: AssetId) -> bool { + false + } +} + +/// This trait indicates a balance that is _on hold_ for an asset account. +/// +/// A balance _on hold_ is a balance that, while is assigned to an account, +/// is outside the direct control of it. Instead, is being _held_ by the +/// system logic (i.e. Pallets) and can be eventually burned or released. +pub trait BalanceOnHold { + /// Return the held balance. + /// + /// If `Some`, it means some balance is _on hold_, and it can be + /// infallibly burned. + /// + /// If `None` is returned, then no balance is _on hold_ for `who`'s asset + /// account. + fn balance_on_hold(asset: AssetId, who: &AccountId) -> Option; + + /// Called after an account has been removed. + /// + /// It is expected that this method is called only when there is no balance + /// on hold. Otherwise, an account should not be removed. + fn died(asset: AssetId, who: &AccountId); + + /// Return a value that indicates if there are registered holds for a given asset. + fn contains_holds(asset: AssetId) -> bool; +} + +impl BalanceOnHold for () { + fn balance_on_hold(_: AssetId, _: &AccountId) -> Option { + None + } + fn died(_: AssetId, _: &AccountId) {} + fn contains_holds(_: AssetId) -> bool { + false + } } #[derive(Copy, Clone, PartialEq, Eq)] diff --git a/substrate/frame/contracts/mock-network/src/parachain.rs b/substrate/frame/contracts/mock-network/src/parachain.rs index 6edbfb0e7e86..f356b314c44b 100644 --- a/substrate/frame/contracts/mock-network/src/parachain.rs +++ b/substrate/frame/contracts/mock-network/src/parachain.rs @@ -120,6 +120,7 @@ impl pallet_assets::Config for Runtime { type AssetAccountDeposit = AssetAccountDeposit; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = (); diff --git a/substrate/frame/nft-fractionalization/src/mock.rs b/substrate/frame/nft-fractionalization/src/mock.rs index 50b41b5fc64e..1cd44eff7056 100644 --- a/substrate/frame/nft-fractionalization/src/mock.rs +++ b/substrate/frame/nft-fractionalization/src/mock.rs @@ -77,6 +77,7 @@ impl pallet_assets::Config for Test { type MetadataDepositPerByte = ConstU64<1>; type ApprovalDeposit = ConstU64<1>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = (); type Extra = (); type CallbackHandle = (); diff --git a/substrate/frame/revive/mock-network/src/parachain.rs b/substrate/frame/revive/mock-network/src/parachain.rs index 26a8fdcada27..e670f91aec2c 100644 --- a/substrate/frame/revive/mock-network/src/parachain.rs +++ b/substrate/frame/revive/mock-network/src/parachain.rs @@ -120,6 +120,7 @@ impl pallet_assets::Config for Runtime { type AssetAccountDeposit = AssetAccountDeposit; type ApprovalDeposit = ApprovalDeposit; type StringLimit = AssetsStringLimit; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = (); diff --git a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs index a86b86c223ef..b1482b77fff5 100644 --- a/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs +++ b/substrate/frame/transaction-payment/asset-conversion-tx-payment/src/mock.rs @@ -195,6 +195,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ConstU64<0>; type ApprovalDeposit = ConstU64<0>; type StringLimit = ConstU32<20>; + type Holder = (); type Freezer = (); type Extra = (); type CallbackHandle = (); @@ -220,6 +221,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ConstU64<0>; type ApprovalDeposit = ConstU64<0>; type StringLimit = ConstU32<50>; + type Holder = (); type Freezer = (); type Extra = (); type WeightInfo = (); diff --git a/substrate/frame/transaction-payment/asset-tx-payment/src/mock.rs b/substrate/frame/transaction-payment/asset-tx-payment/src/mock.rs index fce029bb4bfc..302536e5264b 100644 --- a/substrate/frame/transaction-payment/asset-tx-payment/src/mock.rs +++ b/substrate/frame/transaction-payment/asset-tx-payment/src/mock.rs @@ -139,6 +139,7 @@ impl pallet_assets::Config for Runtime { type MetadataDepositPerByte = ConstU64<0>; type ApprovalDeposit = ConstU64<0>; type StringLimit = ConstU32<20>; + type Holder = (); type Freezer = (); type Extra = (); type CallbackHandle = (); diff --git a/umbrella/Cargo.toml b/umbrella/Cargo.toml index 7f50658c4e16..b938a9f392ef 100644 --- a/umbrella/Cargo.toml +++ b/umbrella/Cargo.toml @@ -58,6 +58,7 @@ std = [ "pallet-asset-rate?/std", "pallet-asset-tx-payment?/std", "pallet-assets-freezer?/std", + "pallet-assets-holder?/std", "pallet-assets?/std", "pallet-atomic-swap?/std", "pallet-aura?/std", @@ -257,6 +258,7 @@ runtime-benchmarks = [ "pallet-asset-rate?/runtime-benchmarks", "pallet-asset-tx-payment?/runtime-benchmarks", "pallet-assets-freezer?/runtime-benchmarks", + "pallet-assets-holder?/runtime-benchmarks", "pallet-assets?/runtime-benchmarks", "pallet-babe?/runtime-benchmarks", "pallet-bags-list?/runtime-benchmarks", @@ -386,6 +388,7 @@ try-runtime = [ "pallet-asset-rate?/try-runtime", "pallet-asset-tx-payment?/try-runtime", "pallet-assets-freezer?/try-runtime", + "pallet-assets-holder?/try-runtime", "pallet-assets?/try-runtime", "pallet-atomic-swap?/try-runtime", "pallet-aura?/try-runtime", @@ -541,7 +544,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-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-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-fixtures", "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-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-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-assets-holder", "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-fixtures", "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", @@ -872,6 +875,11 @@ path = "../substrate/frame/assets-freezer" default-features = false optional = true +[dependencies.pallet-assets-holder] +path = "../substrate/frame/assets-holder" +default-features = false +optional = true + [dependencies.pallet-atomic-swap] path = "../substrate/frame/atomic-swap" default-features = false diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs index 2216864fad0f..8395db1863ac 100644 --- a/umbrella/src/lib.rs +++ b/umbrella/src/lib.rs @@ -320,6 +320,10 @@ pub use pallet_assets; #[cfg(feature = "pallet-assets-freezer")] pub use pallet_assets_freezer; +/// Provides holding features to `pallet-assets`. +#[cfg(feature = "pallet-assets-holder")] +pub use pallet_assets_holder; + /// FRAME atomic swap pallet. #[cfg(feature = "pallet-atomic-swap")] pub use pallet_atomic_swap;