diff --git a/Cargo.lock b/Cargo.lock
index f3ffba47d0eb24009aa3b69ec7ed696bed4835ee..79d6f1975327b27bb08c54e5e237584e988a5761 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -12168,6 +12168,23 @@ dependencies = [
  "sp-runtime 39.0.2",
 ]
 
+[[package]]
+name = "pallet-assets-holder"
+version = "0.1.0"
+dependencies = [
+ "frame-benchmarking 28.0.0",
+ "frame-support 28.0.0",
+ "frame-system 28.0.0",
+ "log",
+ "pallet-assets 29.1.0",
+ "pallet-balances 28.0.0",
+ "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"
@@ -18759,6 +18776,7 @@ dependencies = [
  "pallet-asset-tx-payment 28.0.0",
  "pallet-assets 29.1.0",
  "pallet-assets-freezer 0.1.0",
+ "pallet-assets-holder",
  "pallet-atomic-swap 28.0.0",
  "pallet-aura 27.0.0",
  "pallet-authority-discovery 28.0.0",
diff --git a/Cargo.toml b/Cargo.toml
index b3a19006e94f4cef9c2b7b0cb8f4e30196c70b51..1e7bec5e83194c7a952e3073510b7a59d535875b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -318,6 +318,7 @@ members = [
 	"substrate/frame/asset-rewards",
 	"substrate/frame/assets",
 	"substrate/frame/assets-freezer",
+	"substrate/frame/assets-holder",
 	"substrate/frame/atomic-swap",
 	"substrate/frame/aura",
 	"substrate/frame/authority-discovery",
@@ -900,6 +901,7 @@ pallet-asset-rewards = { path = "substrate/frame/asset-rewards", default-feature
 pallet-asset-tx-payment = { path = "substrate/frame/transaction-payment/asset-tx-payment", default-features = false }
 pallet-assets = { path = "substrate/frame/assets", default-features = false }
 pallet-assets-freezer = { path = "substrate/frame/assets-freezer", default-features = false }
+pallet-assets-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 ba40bfd2a3abd979b67a73d9ecbab02eea852b88..6e27aaf88b4bc7d86eab75230f6dcb4651d9a2a7 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/lib.rs
@@ -273,6 +273,7 @@ impl pallet_assets::Config<TrustBackedAssetsInstance> 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<Runtime>;
@@ -318,6 +319,7 @@ impl pallet_assets::Config<PoolAssetsInstance> 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<Runtime>;
@@ -494,6 +496,7 @@ impl pallet_assets::Config<ForeignAssetsInstance> 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<Runtime>;
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 5fb6b522abf1b9a62daab583ed3f6445ce094f11..45b67d5499401e68a055336d863b1b98d6658b85 100644
--- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
+++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/lib.rs
@@ -277,6 +277,7 @@ impl pallet_assets::Config<TrustBackedAssetsInstance> 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<Runtime>;
@@ -321,6 +322,7 @@ impl pallet_assets::Config<PoolAssetsInstance> 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<Runtime>;
@@ -546,6 +548,7 @@ impl pallet_assets::Config<ForeignAssetsInstance> 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<Runtime>;
diff --git a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
index 2b93e391c2e8f760961664560c372d57e0d3c848..ed6e014417d457ba0b51819427ac94db557574e0 100644
--- a/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
+++ b/cumulus/parachains/runtimes/testing/penpal/src/lib.rs
@@ -480,6 +480,7 @@ impl pallet_assets::Config<TrustBackedAssetsInstance> for Runtime {
 	type MetadataDepositPerByte = MetadataDepositPerByte;
 	type ApprovalDeposit = ApprovalDeposit;
 	type StringLimit = AssetsStringLimit;
+	type Holder = ();
 	type Freezer = ();
 	type Extra = ();
 	type WeightInfo = pallet_assets::weights::SubstrateWeight<Runtime>;
@@ -518,6 +519,7 @@ impl pallet_assets::Config<ForeignAssetsInstance> for Runtime {
 	type MetadataDepositPerByte = ForeignAssetsMetadataDepositPerByte;
 	type ApprovalDeposit = ForeignAssetsApprovalDeposit;
 	type StringLimit = ForeignAssetsAssetsStringLimit;
+	type Holder = ();
 	type Freezer = ();
 	type Extra = ();
 	type WeightInfo = pallet_assets::weights::SubstrateWeight<Runtime>;
@@ -557,6 +559,7 @@ impl pallet_assets::Config<PoolAssetsInstance> 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<Runtime>;
diff --git a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs
index f6b96b28a8dd6eb0716ed9483132ad67a14e82e0..0e237ba5c4318b7b940f7cc06a5a54acb2ff912d 100644
--- a/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs
+++ b/cumulus/parachains/runtimes/testing/rococo-parachain/src/lib.rs
@@ -596,6 +596,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<Runtime>;
diff --git a/polkadot/xcm/pallet-xcm/src/mock.rs b/polkadot/xcm/pallet-xcm/src/mock.rs
index 58b4226ccf191d001a0ed3f3178c45b5724cf0be..74d2f4584d63b8b5769bf37f3bd3e419ce749c7c 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 55a924dbaa63e56ed8dbc565dcbe17e2fb4a3d54..d24f19fd36680667ccfd138364532c0bae122b7b 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<TrustBackedAssetsInstance> for Runtime {
 	type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
 	type ForceOrigin = frame_system::EnsureRoot<AccountId>;
 	type Freezer = ();
+	type Holder = ();
 	type CallbackHandle = ();
 }
 
@@ -97,6 +98,7 @@ impl pallet_assets::Config<PoolAssetsInstance> for Runtime {
 	type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
 	type ForceOrigin = frame_system::EnsureRoot<AccountId>;
 	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 6ebf6476f7e570f15a37908c40ea53c9810b2875..3274b07ac2fd7bba63a7357b5b4a479925cfd84f 100644
--- a/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
+++ b/polkadot/xcm/xcm-builder/src/tests/pay/mock.rs
@@ -112,6 +112,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 24e0e3fd4ed271e64e521a30aab2e33df689ed47..8c6c47e7ee909eb7da010ecf4c2a49ea5505db09 100644
--- a/polkadot/xcm/xcm-runtime-apis/tests/mock.rs
+++ b/polkadot/xcm/xcm-runtime-apis/tests/mock.rs
@@ -97,6 +97,7 @@ impl pallet_assets::Config for TestRuntime {
 	type Currency = Balances;
 	type CreateOrigin = AsEnsureOriginWithArg<frame_system::EnsureSigned<AccountId>>;
 	type ForceOrigin = frame_system::EnsureRoot<AccountId>;
+	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 0000000000000000000000000000000000000000..b3da09a2134bae28300e3d4d6a6cee960e9e7cef
--- /dev/null
+++ b/prdoc/pr_4530.prdoc
@@ -0,0 +1,107 @@
+title: "Implement `pallet-assets-holder` and consider ED part of frozen amount in `pallet-assets`"
+
+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 d3dd08369e79e75772e08f14d9dd6eb0b9ae2564..e0404fdc2bc5e6561191cbb7acb130b016cf4179 100644
--- a/substrate/bin/node/runtime/src/lib.rs
+++ b/substrate/bin/node/runtime/src/lib.rs
@@ -2070,6 +2070,7 @@ impl pallet_assets::Config<Instance1> for Runtime {
 	type MetadataDepositPerByte = MetadataDepositPerByte;
 	type ApprovalDeposit = ApprovalDeposit;
 	type StringLimit = StringLimit;
+	type Holder = ();
 	type Freezer = ();
 	type Extra = ();
 	type CallbackHandle = ();
@@ -2097,6 +2098,7 @@ impl pallet_assets::Config<Instance2> for Runtime {
 	type MetadataDepositPerByte = MetadataDepositPerByte;
 	type ApprovalDeposit = ApprovalDeposit;
 	type StringLimit = StringLimit;
+	type Holder = ();
 	type Freezer = ();
 	type Extra = ();
 	type WeightInfo = pallet_assets::weights::SubstrateWeight<Runtime>;
diff --git a/substrate/frame/asset-conversion/ops/src/mock.rs b/substrate/frame/asset-conversion/ops/src/mock.rs
index 576b266b39c17e26d9f4d2837e0ce86ba6360153..1d38ab615745a039ff94adfb5a9ea9435a579130 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<Instance1> for Test {
 	type Currency = Balances;
 	type CreateOrigin = AsEnsureOriginWithArg<EnsureSigned<Self::AccountId>>;
 	type ForceOrigin = frame_system::EnsureRoot<Self::AccountId>;
+	type Holder = ();
 	type Freezer = ();
 }
 
@@ -76,6 +77,7 @@ impl pallet_assets::Config<Instance2> for Test {
 	type CreateOrigin =
 		AsEnsureOriginWithArg<EnsureSignedBy<AssetConversionOrigin, Self::AccountId>>;
 	type ForceOrigin = frame_system::EnsureRoot<Self::AccountId>;
+	type Holder = ();
 	type Freezer = ();
 }
 
diff --git a/substrate/frame/asset-conversion/src/mock.rs b/substrate/frame/asset-conversion/src/mock.rs
index 313d9f9857e49be8c9c18f3f926249839e814d82..75377bb2c277193107ad1f3c35c2911d8efe7430 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<Instance1> 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<Instance2> 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/asset-rewards/src/mock.rs b/substrate/frame/asset-rewards/src/mock.rs
index 1e9b41104d4cd03e5810a5f256fb6a8e0f024ecc..4e92a345aa9be6a61ee02a9946a4ca83bc86dba8 100644
--- a/substrate/frame/asset-rewards/src/mock.rs
+++ b/substrate/frame/asset-rewards/src/mock.rs
@@ -90,6 +90,7 @@ impl pallet_assets::Config<Instance1> for MockRuntime {
 	type ApprovalDeposit = ConstU128<1>;
 	type StringLimit = ConstU32<50>;
 	type Freezer = AssetsFreezer;
+	type Holder = ();
 	type Extra = ();
 	type WeightInfo = ();
 	type CallbackHandle = ();
diff --git a/substrate/frame/asset-rewards/src/tests.rs b/substrate/frame/asset-rewards/src/tests.rs
index 399d6a54c939293e4fdc2c87118d85596e701374..9c01c9ca4df213edca6a0ebe155e9817f3923898 100644
--- a/substrate/frame/asset-rewards/src/tests.rs
+++ b/substrate/frame/asset-rewards/src/tests.rs
@@ -434,8 +434,7 @@ mod stake {
 					Preservation::Expendable,
 					Fortitude::Force,
 				),
-				// - extra 1 for ed
-				initial_balance - 1000 - 1
+				initial_balance - 1000
 			);
 
 			// User stakes more tokens
@@ -460,8 +459,7 @@ mod stake {
 					Preservation::Expendable,
 					Fortitude::Force,
 				),
-				// - extra 1 for ed
-				initial_balance - 1500 - 1
+				initial_balance - 1500
 			);
 
 			// Event is emitted.
diff --git a/substrate/frame/assets-freezer/src/impls.rs b/substrate/frame/assets-freezer/src/impls.rs
index 49c0748295d72585994d1d6a4eddaffe870cb38f..59193ab9a6a39cbdd31481a81d5fa1b1ca90cec9 100644
--- a/substrate/frame/assets-freezer/src/impls.rs
+++ b/substrate/frame/assets-freezer/src/impls.rs
@@ -22,6 +22,7 @@
 // SOFTWARE.
 
 use super::*;
+use frame::prelude::storage::StorageDoubleMap;
 use pallet_assets::FrozenBalance;
 
 // Implements [`FrozenBalance`] from [`pallet-assets`], so it can understand how much of an
@@ -35,9 +36,22 @@ impl<T: Config<I>, I: 'static> FrozenBalance<T::AssetId, T::AccountId, T::Balanc
 	}
 
 	fn died(asset: T::AssetId, who: &T::AccountId) {
+		defensive_assert!(
+			Freezes::<T, I>::get(asset.clone(), who).is_empty(),
+			"The list of Freezes should be empty before allowing an account to die"
+		);
+		defensive_assert!(
+			FrozenBalances::<T, I>::get(asset.clone(), who).is_none(),
+			"There should not be a frozen balance before allowing to die"
+		);
+
 		FrozenBalances::<T, I>::remove(asset.clone(), who);
 		Freezes::<T, I>::remove(asset, who);
 	}
+
+	fn contains_freezes(asset: T::AssetId) -> bool {
+		Freezes::<T, I>::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 70c77e6bf8beb5bff5d85f99e8f3b3ea33a2b596..c957692f2b48ec9554f3a9ed1cfcb470cb5cc575 100644
--- a/substrate/frame/assets-freezer/src/mock.rs
+++ b/substrate/frame/assets-freezer/src/mock.rs
@@ -104,6 +104,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 41a18e01f8ebb774bd52d59f6dc6f302937015d3..dc78670be7999e3b50cf9bb5b35f9d54bd5f8e1a 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::<Test>::get(ASSET_ID, WHO).is_none());
 			assert!(Freezes::<Test>::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::<Test>::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::<Test>::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(
@@ -293,10 +303,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::<Test>::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 0000000000000000000000000000000000000000..cb0de25f95de85ac65cdb031271f6e6cd5e4b7ef
--- /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 = "Apache-2.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 }
+frame-benchmarking = { optional = true, workspace = true }
+frame-support = { workspace = true }
+frame-system = { workspace = true }
+log = { workspace = true }
+pallet-assets = { workspace = true }
+scale-info = { features = ["derive"], workspace = true }
+sp-runtime = { workspace = true }
+
+[dev-dependencies]
+pallet-balances = { workspace = true }
+sp-core = { workspace = true }
+sp-io = { 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 0000000000000000000000000000000000000000..b286cbb2eb49a341856fb8f52f8514e65225c2ba
--- /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<T: Config<I>, I: 'static> BalanceOnHold<T::AssetId, T::AccountId, T::Balance>
+	for Pallet<T, I>
+{
+	fn balance_on_hold(asset: T::AssetId, who: &T::AccountId) -> Option<T::Balance> {
+		BalancesOnHold::<T, I>::get(asset, who)
+	}
+
+	fn died(asset: T::AssetId, who: &T::AccountId) {
+		defensive_assert!(
+			Holds::<T, I>::get(asset.clone(), who).is_empty(),
+			"The list of Holds should be empty before allowing an account to die"
+		);
+		defensive_assert!(
+			BalancesOnHold::<T, I>::get(asset.clone(), who).is_none(),
+			"The should not be a balance on hold before allowing to die"
+		);
+
+		Holds::<T, I>::remove(asset.clone(), who);
+		BalancesOnHold::<T, I>::remove(asset, who);
+	}
+
+	fn contains_holds(asset: T::AssetId) -> bool {
+		Holds::<T, I>::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<T: Config<I>, I: 'static> Inspect<T::AccountId> for Pallet<T, I> {
+	type AssetId = T::AssetId;
+	type Balance = T::Balance;
+
+	fn total_issuance(asset: Self::AssetId) -> Self::Balance {
+		pallet_assets::Pallet::<T, I>::total_issuance(asset)
+	}
+
+	fn minimum_balance(asset: Self::AssetId) -> Self::Balance {
+		pallet_assets::Pallet::<T, I>::minimum_balance(asset)
+	}
+
+	fn total_balance(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance {
+		pallet_assets::Pallet::<T, I>::total_balance(asset, who)
+	}
+
+	fn balance(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance {
+		pallet_assets::Pallet::<T, I>::balance(asset, who)
+	}
+
+	fn reducible_balance(
+		asset: Self::AssetId,
+		who: &T::AccountId,
+		preservation: Preservation,
+		force: Fortitude,
+	) -> Self::Balance {
+		pallet_assets::Pallet::<T, I>::reducible_balance(asset, who, preservation, force)
+	}
+
+	fn can_deposit(
+		asset: Self::AssetId,
+		who: &T::AccountId,
+		amount: Self::Balance,
+		provenance: Provenance,
+	) -> DepositConsequence {
+		pallet_assets::Pallet::<T, I>::can_deposit(asset, who, amount, provenance)
+	}
+
+	fn can_withdraw(
+		asset: Self::AssetId,
+		who: &T::AccountId,
+		amount: Self::Balance,
+	) -> WithdrawConsequence<Self::Balance> {
+		pallet_assets::Pallet::<T, I>::can_withdraw(asset, who, amount)
+	}
+
+	fn asset_exists(asset: Self::AssetId) -> bool {
+		pallet_assets::Pallet::<T, I>::asset_exists(asset)
+	}
+}
+
+impl<T: Config<I>, I: 'static> InspectHold<T::AccountId> for Pallet<T, I> {
+	type Reason = T::RuntimeHoldReason;
+
+	fn total_balance_on_hold(asset: Self::AssetId, who: &T::AccountId) -> Self::Balance {
+		BalancesOnHold::<T, I>::get(asset, who).unwrap_or_else(Zero::zero)
+	}
+
+	fn balance_on_hold(
+		asset: Self::AssetId,
+		reason: &Self::Reason,
+		who: &T::AccountId,
+	) -> Self::Balance {
+		Holds::<T, I>::get(asset, who)
+			.iter()
+			.find(|x| &x.id == reason)
+			.map(|x| x.amount)
+			.unwrap_or_else(Zero::zero)
+	}
+}
+
+impl<T: Config<I>, I: 'static> Unbalanced<T::AccountId> for Pallet<T, I> {
+	fn handle_dust(dust: Dust<T::AccountId, Self>) {
+		let Dust(id, balance) = dust;
+		pallet_assets::Pallet::<T, I>::handle_dust(Dust(id, balance));
+	}
+
+	fn write_balance(
+		asset: Self::AssetId,
+		who: &T::AccountId,
+		amount: Self::Balance,
+	) -> Result<Option<Self::Balance>, DispatchError> {
+		pallet_assets::Pallet::<T, I>::write_balance(asset, who, amount)
+	}
+
+	fn set_total_issuance(asset: Self::AssetId, amount: Self::Balance) {
+		pallet_assets::Pallet::<T, I>::set_total_issuance(asset, amount)
+	}
+
+	fn decrease_balance(
+		asset: Self::AssetId,
+		who: &T::AccountId,
+		amount: Self::Balance,
+		precision: Precision,
+		preservation: Preservation,
+		force: Fortitude,
+	) -> Result<Self::Balance, DispatchError> {
+		pallet_assets::Pallet::<T, I>::decrease_balance(
+			asset,
+			who,
+			amount,
+			precision,
+			preservation,
+			force,
+		)
+	}
+
+	fn increase_balance(
+		asset: Self::AssetId,
+		who: &T::AccountId,
+		amount: Self::Balance,
+		precision: Precision,
+	) -> Result<Self::Balance, DispatchError> {
+		pallet_assets::Pallet::<T, I>::increase_balance(asset, who, amount, precision)
+	}
+}
+
+impl<T: Config<I>, I: 'static> UnbalancedHold<T::AccountId> for Pallet<T, I> {
+	fn set_balance_on_hold(
+		asset: Self::AssetId,
+		reason: &Self::Reason,
+		who: &T::AccountId,
+		amount: Self::Balance,
+	) -> DispatchResult {
+		let mut holds = Holds::<T, I>::get(asset.clone(), who);
+		let amount_on_hold =
+			BalancesOnHold::<T, I>::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::<T, I>::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::<T, I>::insert(asset.clone(), who, holds);
+		} else {
+			Holds::<T, I>::remove(asset.clone(), who);
+		}
+
+		if amount_on_hold.is_zero() {
+			BalancesOnHold::<T, I>::remove(asset.clone(), who);
+		} else {
+			BalancesOnHold::<T, I>::insert(asset.clone(), who, amount_on_hold);
+		}
+
+		Ok(())
+	}
+}
+
+impl<T: Config<I>, I: 'static> MutateHold<T::AccountId> for Pallet<T, I> {
+	fn done_hold(
+		asset_id: Self::AssetId,
+		reason: &Self::Reason,
+		who: &T::AccountId,
+		amount: Self::Balance,
+	) {
+		Self::deposit_event(Event::<T, I>::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::<T, I>::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::<T, I>::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 0000000000000000000000000000000000000000..ac63a252daaf1027becf5b723897264d682a6e6c
--- /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<I: 'static = ()>:
+		frame_system::Config + pallet_assets::Config<I, Holder = Pallet<Self, I>>
+	{
+		/// 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<Event<Self, I>>
+			+ IsType<<Self as frame_system::Config>::RuntimeEvent>;
+	}
+
+	#[pallet::error]
+	pub enum Error<T, I = ()> {
+		/// Number of holds on an account would exceed the count of `RuntimeHoldReason`.
+		TooManyHolds,
+	}
+
+	#[pallet::pallet]
+	pub struct Pallet<T, I = ()>(_);
+
+	#[pallet::event]
+	#[pallet::generate_deposit(pub(super) fn deposit_event)]
+	pub enum Event<T: Config<I>, 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<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
+		_,
+		Blake2_128Concat,
+		T::AssetId,
+		Blake2_128Concat,
+		T::AccountId,
+		BoundedVec<
+			IdAmount<T::RuntimeHoldReason, T::Balance>,
+			VariantCountOf<T::RuntimeHoldReason>,
+		>,
+		ValueQuery,
+	>;
+
+	/// A map that stores the current total balance on hold for every account on a given AssetId.
+	#[pallet::storage]
+	pub(super) type BalancesOnHold<T: Config<I>, I: 'static = ()> = StorageDoubleMap<
+		_,
+		Blake2_128Concat,
+		T::AssetId,
+		Blake2_128Concat,
+		T::AccountId,
+		T::Balance,
+	>;
+
+	#[pallet::hooks]
+	impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
+		#[cfg(feature = "try-runtime")]
+		fn try_state(_: BlockNumberFor<T>) -> Result<(), sp_runtime::TryRuntimeError> {
+			Self::do_try_state()
+		}
+	}
+}
+
+impl<T: Config<I>, I: 'static> Pallet<T, I> {
+	#[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::<T, I>::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::<T, I>::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 0000000000000000000000000000000000000000..8d9ea1f51a3d9eb3d1bf5b3366a4c0616e6fb244
--- /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 = <Test as frame_system::Config>::AccountId;
+pub type Balance = <Test as pallet_balances::Config>::Balance;
+pub type AssetId = <Test as pallet_assets::Config>::AssetId;
+type Block = frame_system::mocking::MockBlock<Test>;
+
+#[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<u64>;
+}
+
+#[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<frame_system::EnsureSigned<u64>>;
+	type ForceOrigin = frame_system::EnsureRoot<u64>;
+	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 0000000000000000000000000000000000000000..433ed664a144500769c9079e66c2d0450f7dc3c8
--- /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!(
+				<AssetsHolder as BalanceOnHold<_, _, _>>::balance_on_hold(ASSET_ID, &WHO),
+				None
+			);
+			test_hold(DummyHoldReason::Governance, 1);
+			assert_eq!(
+				<AssetsHolder as BalanceOnHold<_, _, _>>::balance_on_hold(ASSET_ID, &WHO),
+				Some(1u64)
+			);
+			test_hold(DummyHoldReason::Staking, 3);
+			assert_eq!(
+				<AssetsHolder as BalanceOnHold<_, _, _>>::balance_on_hold(ASSET_ID, &WHO),
+				Some(4u64)
+			);
+			test_hold(DummyHoldReason::Governance, 2);
+			assert_eq!(
+				<AssetsHolder as BalanceOnHold<_, _, _>>::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!(
+				<AssetsHolder as BalanceOnHold<_, _, _>>::balance_on_hold(ASSET_ID, &WHO),
+				Some(3u64)
+			);
+			test_release(DummyHoldReason::Staking);
+			assert_eq!(
+				<AssetsHolder as BalanceOnHold<_, _, _>>::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::<Test>::get(ASSET_ID, WHO).is_none());
+			assert!(Holds::<Test>::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!(
+				<AssetsHolder as InspectHold<_>>::balance_on_hold(
+					ASSET_ID,
+					&DummyHoldReason::Governance,
+					&WHO
+				),
+				0u64
+			);
+			test_hold(DummyHoldReason::Governance, 1);
+			assert_eq!(
+				<AssetsHolder as InspectHold<_>>::balance_on_hold(
+					ASSET_ID,
+					&DummyHoldReason::Governance,
+					&WHO
+				),
+				1u64
+			);
+			test_hold(DummyHoldReason::Staking, 3);
+			assert_eq!(
+				<AssetsHolder as InspectHold<_>>::balance_on_hold(
+					ASSET_ID,
+					&DummyHoldReason::Staking,
+					&WHO
+				),
+				3u64
+			);
+			test_hold(DummyHoldReason::Staking, 2);
+			assert_eq!(
+				<AssetsHolder as InspectHold<_>>::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!(
+				<AssetsHolder as InspectHold<_>>::balance_on_hold(
+					ASSET_ID,
+					&DummyHoldReason::Governance,
+					&WHO
+				),
+				0u64
+			);
+			test_release(DummyHoldReason::Staking);
+			assert_eq!(
+				<AssetsHolder as InspectHold<_>>::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::<Test>::get(ASSET_ID, WHO).to_vec(), vec![]);
+			assert_eq!(BalancesOnHold::<Test>::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::<Test>::get(ASSET_ID, WHO).to_vec(),
+				vec![IdAmount { id: DummyHoldReason::Governance, amount: 1 }]
+			);
+			assert_eq!(BalancesOnHold::<Test>::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::<Test>::get(ASSET_ID, WHO).to_vec(),
+				vec![IdAmount { id: DummyHoldReason::Governance, amount: 3 }]
+			);
+			assert_eq!(BalancesOnHold::<Test>::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::<Test>::get(ASSET_ID, WHO).to_vec(),
+				vec![
+					IdAmount { id: DummyHoldReason::Governance, amount: 3 },
+					IdAmount { id: DummyHoldReason::Staking, amount: 2 }
+				]
+			);
+			assert_eq!(BalancesOnHold::<Test>::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::<Test>::TooManyHolds
+			// );
+
+			// Decreasing balance on hold works
+			assert_ok!(AssetsHolder::set_balance_on_hold(
+				ASSET_ID,
+				&DummyHoldReason::Staking,
+				&WHO,
+				1
+			));
+			assert_eq!(
+				Holds::<Test>::get(ASSET_ID, WHO).to_vec(),
+				vec![
+					IdAmount { id: DummyHoldReason::Governance, amount: 3 },
+					IdAmount { id: DummyHoldReason::Staking, amount: 1 }
+				]
+			);
+			assert_eq!(BalancesOnHold::<Test>::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::<Test>::get(ASSET_ID, WHO).to_vec(),
+				vec![IdAmount { id: DummyHoldReason::Staking, amount: 1 }]
+			);
+			assert_eq!(BalancesOnHold::<Test>::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::<Test>::get(ASSET_ID, WHO).to_vec(), vec![]);
+			assert_eq!(BalancesOnHold::<Test>::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!(
+				<AssetsHolder as InspectHold<_>>::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!(
+				<AssetsHolder as InspectHold<_>>::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!(
+				<AssetsHolder as InspectHold<_>>::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!(
+				<AssetsHolder as InspectHold<_>>::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!(
+				<AssetsHolder as InspectHold<_>>::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::<Test>::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::<Test>::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::<Test>::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 c218c4ddc952cc4f6ef3a7be652e7000660c1150..704707b245ffcc5aed82bd2f675ae0e5176df360 100644
--- a/substrate/frame/assets/src/functions.rs
+++ b/substrate/frame/assets/src/functions.rs
@@ -95,6 +95,15 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 		Ok(reason)
 	}
 
+	pub(super) fn ensure_account_can_die(id: T::AssetId, who: &T::AccountId) -> DispatchResult {
+		ensure!(
+			T::Holder::balance_on_hold(id.clone(), who).is_none(),
+			Error::<T, I>::ContainsHolds
+		);
+		ensure!(T::Freezer::frozen_balance(id, who).is_none(), Error::<T, I>::ContainsFreezes);
+		Ok(())
+	}
+
 	pub(super) fn dead_account(
 		who: &T::AccountId,
 		d: &mut AssetDetails<T::Balance, T::AccountId, DepositBalanceOf<T, I>>,
@@ -102,6 +111,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 		force: bool,
 	) -> DeadConsequence {
 		use ExistenceReason::*;
+
 		match *reason {
 			Consumer => frame_system::Pallet::<T>::dec_consumers(who),
 			Sufficient => {
@@ -193,22 +203,37 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 			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 +253,21 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 		let account = Account::<T, I>::get(&id, who).ok_or(Error::<T, I>::NoAccount)?;
 		ensure!(!account.status.is_frozen(), Error::<T, I>::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,11 +377,13 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 	pub(super) fn do_refund(id: T::AssetId, who: T::AccountId, allow_burn: bool) -> DispatchResult {
 		use AssetStatus::*;
 		use ExistenceReason::*;
+
 		let mut account = Account::<T, I>::get(&id, &who).ok_or(Error::<T, I>::NoDeposit)?;
 		ensure!(matches!(account.reason, Consumer | DepositHeld(..)), Error::<T, I>::NoDeposit);
 		let mut details = Asset::<T, I>::get(&id).ok_or(Error::<T, I>::Unknown)?;
 		ensure!(matches!(details.status, Live | Frozen), Error::<T, I>::IncorrectStatus);
 		ensure!(account.balance.is_zero() || allow_burn, Error::<T, I>::WouldBurn);
+		Self::ensure_account_can_die(id.clone(), &who)?;
 
 		if let Some(deposit) = account.reason.take_deposit() {
 			T::Currency::unreserve(&who, deposit);
@@ -369,9 +397,11 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 			Account::<T, I>::insert(id, &who, account);
 			return Ok(())
 		}
+
 		Asset::<T, I>::insert(&id, details);
 		// Executing a hook here is safe, since it is not in a `mutate`.
-		T::Freezer::died(id, &who);
+		T::Freezer::died(id.clone(), &who);
+		T::Holder::died(id, &who);
 		Ok(())
 	}
 
@@ -394,6 +424,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 			ensure!(caller == depositor || caller == details.admin, Error::<T, I>::NoPermission);
 		}
 		ensure!(account.balance.is_zero(), Error::<T, I>::WouldBurn);
+		Self::ensure_account_can_die(id.clone(), who)?;
 
 		T::Currency::unreserve(&depositor, deposit);
 
@@ -407,7 +438,8 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 		}
 		Asset::<T, I>::insert(&id, details);
 		// Executing a hook here is safe, since it is not in a `mutate`.
-		T::Freezer::died(id, &who);
+		T::Freezer::died(id.clone(), who);
+		T::Holder::died(id, &who);
 		return Ok(())
 	}
 
@@ -561,6 +593,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 				account.balance = account.balance.saturating_sub(actual);
 				if account.balance < details.min_balance {
 					debug_assert!(account.balance.is_zero(), "checked in prep; qed");
+					Self::ensure_account_can_die(id.clone(), target)?;
 					target_died = Some(Self::dead_account(target, details, &account.reason, false));
 					if let Some(Remove) = target_died {
 						return Ok(())
@@ -575,7 +608,8 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 
 		// 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 +633,8 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 		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,11 +689,17 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 			debug_assert!(source_account.balance >= debit, "checked in prep; qed");
 			source_account.balance = source_account.balance.saturating_sub(debit);
 
+			// Pre-check that an account can die if is below min balance
+			if source_account.balance < details.min_balance {
+				debug_assert!(source_account.balance.is_zero(), "checked in prep; qed");
+				Self::ensure_account_can_die(id.clone(), source)?;
+			}
+
 			Account::<T, I>::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.
+						// 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"
@@ -753,6 +794,10 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 			if let Some(check_owner) = maybe_check_owner {
 				ensure!(details.owner == check_owner, Error::<T, I>::NoPermission);
 			}
+
+			ensure!(!T::Holder::contains_holds(id.clone()), Error::<T, I>::ContainsHolds);
+			ensure!(!T::Freezer::contains_freezes(id.clone()), Error::<T, I>::ContainsFreezes);
+
 			details.status = AssetStatus::Destroying;
 
 			Self::deposit_event(Event::DestructionStarted { asset_id: id });
@@ -775,7 +820,11 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 				let mut details = maybe_details.as_mut().ok_or(Error::<T, I>::Unknown)?;
 				// Should only destroy accounts while the asset is in a destroying state
 				ensure!(details.status == AssetStatus::Destroying, Error::<T, I>::IncorrectStatus);
+
 				for (i, (who, mut v)) in Account::<T, I>::iter_prefix(&id).enumerate() {
+					if Self::ensure_account_can_die(id.clone(), &who).is_err() {
+						continue
+					}
 					// unreserve the existence deposit if any
 					if let Some((depositor, deposit)) = v.reason.take_deposit_from() {
 						T::Currency::unreserve(&depositor, deposit);
@@ -800,6 +849,7 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 
 		for who in &dead_accounts {
 			T::Freezer::died(id.clone(), &who);
+			T::Holder::died(id.clone(), &who);
 		}
 
 		Self::deposit_event(Event::AccountsDestroyed {
@@ -960,7 +1010,8 @@ impl<T: Config<I>, I: 'static> Pallet<T, I> {
 
 		// 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 578fa08c4e63ed8cc7dfedea21608228e03c6415..6ab7e941ea1afe81dfa1b6912e3ec56837ad0db6 100644
--- a/substrate/frame/assets/src/impl_fungibles.rs
+++ b/substrate/frame/assets/src/impl_fungibles.rs
@@ -47,7 +47,8 @@ impl<T: Config<I>, I: 'static> fungibles::Inspect<<T as SystemConfig>::AccountId
 	}
 
 	fn total_balance(asset: Self::AssetId, who: &<T as SystemConfig>::AccountId) -> Self::Balance {
-		Pallet::<T, I>::balance(asset, who)
+		Pallet::<T, I>::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 a9b0dc950a6101185d4afc89ce5dedbba95cb6d6..9ea346c4cf3fde31c26fa5e940618ce473b693ee 100644
--- a/substrate/frame/assets/src/lib.rs
+++ b/substrate/frame/assets/src/lib.rs
@@ -295,6 +295,8 @@ pub mod pallet {
 			type MetadataDepositPerByte = ConstUint<1>;
 			type ApprovalDeposit = ConstUint<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<Self::AssetId, Self::AccountId, Self::Balance>;
 
+		/// A hook to inspect a per-asset, per-account balance that is held. This goes in
+		/// accordance with balance model.
+		type Holder: BalanceOnHold<Self::AssetId, Self::AccountId, Self::Balance>;
+
 		/// 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(<T as Config<I>>::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<T>, 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 2c160840e147829f462ec3f407f00afdd77e9dda..9803f929a566f7821f09e06a2a6a6f57378d981e 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<frame_system::EnsureSigned<u64>>;
 	type ForceOrigin = frame_system::EnsureRoot<u64>;
 	type Freezer = TestFreezer;
+	type Holder = TestHolder;
 	type CallbackHandle = (AssetsCallbackHandle, AutoIncAssetId<Test>);
 }
 
@@ -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<Hook> = Default::default();
 }
 
+pub struct TestHolder;
+impl BalanceOnHold<u32, u64, u64> for TestHolder {
+	fn balance_on_hold(asset: u32, who: &u64) -> Option<u64> {
+		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<u32, u64, u64> for TestFreezer {
 	fn frozen_balance(asset: u32, who: &u64) -> Option<u64> {
@@ -129,6 +171,11 @@ impl FrozenBalance<u32, u64, u64> 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 75a6139702c65e3f0dace0b94ecdaa8b0b2bcafe..0b6f55a9af8276e0c5372f93e9f7157e93031084 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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::BalanceLow
+		);
+		assert_noop!(
+			Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 100),
+			Error::<Test>::BalanceLow
+		);
+		assert_noop!(
+			Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 100),
+			Error::<Test>::BalanceLow
+		);
+		// Cannot start destroying the asset, because some accounts contain freezes
+		assert_noop!(
+			Assets::start_destroy(RuntimeOrigin::signed(1), 0),
+			Error::<Test>::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::<Test>::BalanceLow
+		);
+		assert_noop!(
+			Assets::transfer_keep_alive(RuntimeOrigin::signed(1), 0, 2, 100),
+			Error::<Test>::BalanceLow
+		);
+		assert_noop!(
+			Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 100),
+			Error::<Test>::BalanceLow
+		);
+		// Cannot start destroying the asset, because some accounts contain freezes
+		assert_noop!(
+			Assets::start_destroy(RuntimeOrigin::signed(1), 0),
+			Error::<Test>::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::<Test>::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::<Test>::ContainsFreezes
+		);
+
+		// Assert touch deposit is not tainted.
+		let deposit_after_noop =
+			Account::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::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::<Test>::BalanceLow
+		);
+		// Also forced transfers fail
+		assert_noop!(
+			Assets::force_transfer(RuntimeOrigin::signed(1), 0, 1, 2, 29),
+			Error::<Test>::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::<Test>::ContainsFreezes
+		);
+
+		set_balance_on_hold(0, 1, 50);
+		assert_noop!(
+			Assets::start_destroy(RuntimeOrigin::signed(1), 0),
+			Error::<Test>::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 11edc7d3fcb585773bc2d49349948fd56299e7d3..9a60a13f5a71c993d460666f860d8e64731267a3 100644
--- a/substrate/frame/assets/src/types.rs
+++ b/substrate/frame/assets/src/types.rs
@@ -174,7 +174,10 @@ impl AccountStatus {
 
 #[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)]
 pub struct AssetAccount<Balance, DepositBalance, Extra, AccountId> {
-	/// The balance.
+	/// The account's balance.
+	///
+	/// The part of the `balance` may be frozen by the [`Config::Freezer`]. The on-hold portion is
+	/// not included here and is tracked by the [`Config::Holder`].
 	pub(super) balance: Balance,
 	/// The status of the account.
 	pub(super) status: AccountStatus,
@@ -220,9 +223,10 @@ pub trait FrozenBalance<AssetId, AccountId, Balance> {
 	fn frozen_balance(asset: AssetId, who: &AccountId) -> Option<Balance>;
 
 	/// 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<AssetId, AccountId, Balance> FrozenBalance<AssetId, AccountId, Balance> for () {
@@ -230,6 +234,44 @@ impl<AssetId, AccountId, Balance> FrozenBalance<AssetId, AccountId, Balance> 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<AssetId, AccountId, Balance> {
+	/// 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<Balance>;
+
+	/// 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<AssetId, AccountId, Balance> BalanceOnHold<AssetId, AccountId, Balance> for () {
+	fn balance_on_hold(_: AssetId, _: &AccountId) -> Option<Balance> {
+		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 3d192bf7b2422e9b9faa5728c0ba6971c092d066..6e422927cb2b562d1ab2acf93c48e60a92d5e763 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 762c1776e30f195c906c588145fee9015f170db6..0052bf72568d806a07d4a90332bbf75b488b9ef2 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 ea9a26e65b0973a2b0677371feea91b8842372a4..77a9b912f9a665905a691b4d26f0c7b7e205db83 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 a86b86c223ef3289e3d41d69d87a83b15658dfe1..b1482b77fff5781534ea4e10c875e31d0fa1d852 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<Instance2> 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 fce029bb4bfc7e7d3f3541c247b47331fc8f3e49..302536e5264bf16dd136a8e46005d64478ee2737 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 80b72febfb59845fed3a223bfddc70e6a22d2a49..8cd5cf0c838e6987baa662f21f8c36666360643f 100644
--- a/umbrella/Cargo.toml
+++ b/umbrella/Cargo.toml
@@ -60,6 +60,7 @@ std = [
 	"pallet-asset-rewards?/std",
 	"pallet-asset-tx-payment?/std",
 	"pallet-assets-freezer?/std",
+	"pallet-assets-holder?/std",
 	"pallet-assets?/std",
 	"pallet-atomic-swap?/std",
 	"pallet-aura?/std",
@@ -261,6 +262,7 @@ runtime-benchmarks = [
 	"pallet-asset-rewards?/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",
@@ -393,6 +395,7 @@ try-runtime = [
 	"pallet-asset-rewards?/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",
@@ -549,7 +552,7 @@ with-tracing = [
 	"sp-tracing?/with-tracing",
 	"sp-tracing?/with-tracing",
 ]
-runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-weight-reclaim", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-rewards", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-atomic-swap", "pallet-aura", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", "pallet-bags-list", "pallet-balances", "pallet-beefy", "pallet-beefy-mmr", "pallet-bounties", "pallet-bridge-grandpa", "pallet-bridge-messages", "pallet-bridge-parachains", "pallet-bridge-relayers", "pallet-broker", "pallet-child-bounties", "pallet-collator-selection", "pallet-collective", "pallet-collective-content", "pallet-contracts", "pallet-contracts-proc-macro", "pallet-contracts-uapi", "pallet-conviction-voting", "pallet-core-fellowship", "pallet-delegated-staking", "pallet-democracy", "pallet-dev-mode", "pallet-election-provider-multi-block", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"]
+runtime-full = ["assets-common", "binary-merkle-tree", "bp-header-chain", "bp-messages", "bp-parachains", "bp-polkadot", "bp-polkadot-core", "bp-relayers", "bp-runtime", "bp-test-utils", "bp-xcm-bridge-hub", "bp-xcm-bridge-hub-router", "bridge-hub-common", "bridge-runtime-common", "cumulus-pallet-aura-ext", "cumulus-pallet-dmp-queue", "cumulus-pallet-parachain-system", "cumulus-pallet-parachain-system-proc-macro", "cumulus-pallet-session-benchmarking", "cumulus-pallet-solo-to-para", "cumulus-pallet-weight-reclaim", "cumulus-pallet-xcm", "cumulus-pallet-xcmp-queue", "cumulus-ping", "cumulus-primitives-aura", "cumulus-primitives-core", "cumulus-primitives-parachain-inherent", "cumulus-primitives-proof-size-hostfunction", "cumulus-primitives-storage-weight-reclaim", "cumulus-primitives-timestamp", "cumulus-primitives-utility", "frame-benchmarking", "frame-benchmarking-pallet-pov", "frame-election-provider-solution-type", "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", "frame-support-procedural", "frame-support-procedural-tools-derive", "frame-system", "frame-system-benchmarking", "frame-system-rpc-runtime-api", "frame-try-runtime", "pallet-alliance", "pallet-asset-conversion", "pallet-asset-conversion-ops", "pallet-asset-conversion-tx-payment", "pallet-asset-rate", "pallet-asset-rewards", "pallet-asset-tx-payment", "pallet-assets", "pallet-assets-freezer", "pallet-assets-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-block", "pallet-election-provider-multi-phase", "pallet-election-provider-support-benchmarking", "pallet-elections-phragmen", "pallet-fast-unstake", "pallet-glutton", "pallet-grandpa", "pallet-identity", "pallet-im-online", "pallet-indices", "pallet-insecure-randomness-collective-flip", "pallet-lottery", "pallet-membership", "pallet-message-queue", "pallet-migrations", "pallet-mixnet", "pallet-mmr", "pallet-multisig", "pallet-nft-fractionalization", "pallet-nfts", "pallet-nfts-runtime-api", "pallet-nis", "pallet-node-authorization", "pallet-nomination-pools", "pallet-nomination-pools-benchmarking", "pallet-nomination-pools-runtime-api", "pallet-offences", "pallet-offences-benchmarking", "pallet-paged-list", "pallet-parameters", "pallet-preimage", "pallet-proxy", "pallet-ranked-collective", "pallet-recovery", "pallet-referenda", "pallet-remark", "pallet-revive", "pallet-revive-proc-macro", "pallet-revive-uapi", "pallet-root-offences", "pallet-root-testing", "pallet-safe-mode", "pallet-salary", "pallet-scheduler", "pallet-scored-pool", "pallet-session", "pallet-session-benchmarking", "pallet-skip-feeless-payment", "pallet-society", "pallet-staking", "pallet-staking-reward-curve", "pallet-staking-reward-fn", "pallet-staking-runtime-api", "pallet-state-trie-migration", "pallet-statement", "pallet-sudo", "pallet-timestamp", "pallet-tips", "pallet-transaction-payment", "pallet-transaction-payment-rpc-runtime-api", "pallet-transaction-storage", "pallet-treasury", "pallet-tx-pause", "pallet-uniques", "pallet-utility", "pallet-verify-signature", "pallet-vesting", "pallet-whitelist", "pallet-xcm", "pallet-xcm-benchmarks", "pallet-xcm-bridge-hub", "pallet-xcm-bridge-hub-router", "parachains-common", "polkadot-core-primitives", "polkadot-parachain-primitives", "polkadot-primitives", "polkadot-runtime-common", "polkadot-runtime-metrics", "polkadot-runtime-parachains", "polkadot-sdk-frame", "sc-chain-spec-derive", "sc-tracing-proc-macro", "slot-range-helper", "snowbridge-beacon-primitives", "snowbridge-core", "snowbridge-ethereum", "snowbridge-outbound-queue-merkle-tree", "snowbridge-outbound-queue-runtime-api", "snowbridge-pallet-ethereum-client", "snowbridge-pallet-ethereum-client-fixtures", "snowbridge-pallet-inbound-queue", "snowbridge-pallet-inbound-queue-fixtures", "snowbridge-pallet-outbound-queue", "snowbridge-pallet-system", "snowbridge-router-primitives", "snowbridge-runtime-common", "snowbridge-system-runtime-api", "sp-api", "sp-api-proc-macro", "sp-application-crypto", "sp-arithmetic", "sp-authority-discovery", "sp-block-builder", "sp-consensus-aura", "sp-consensus-babe", "sp-consensus-beefy", "sp-consensus-grandpa", "sp-consensus-pow", "sp-consensus-slots", "sp-core", "sp-crypto-ec-utils", "sp-crypto-hashing", "sp-crypto-hashing-proc-macro", "sp-debug-derive", "sp-externalities", "sp-genesis-builder", "sp-inherents", "sp-io", "sp-keyring", "sp-keystore", "sp-metadata-ir", "sp-mixnet", "sp-mmr-primitives", "sp-npos-elections", "sp-offchain", "sp-runtime", "sp-runtime-interface", "sp-runtime-interface-proc-macro", "sp-session", "sp-staking", "sp-state-machine", "sp-statement-store", "sp-std", "sp-storage", "sp-timestamp", "sp-tracing", "sp-transaction-pool", "sp-transaction-storage-proof", "sp-trie", "sp-version", "sp-version-proc-macro", "sp-wasm-interface", "sp-weights", "staging-parachain-info", "staging-xcm", "staging-xcm-builder", "staging-xcm-executor", "substrate-bip39", "testnet-parachains-constants", "tracing-gum-proc-macro", "xcm-procedural", "xcm-runtime-apis"]
 runtime = [
 	"frame-benchmarking",
 	"frame-benchmarking-pallet-pov",
@@ -896,6 +899,11 @@ default-features = false
 optional = true
 path = "../substrate/frame/assets-freezer"
 
+[dependencies.pallet-assets-holder]
+default-features = false
+optional = true
+path = "../substrate/frame/assets-holder"
+
 [dependencies.pallet-atomic-swap]
 default-features = false
 optional = true
diff --git a/umbrella/src/lib.rs b/umbrella/src/lib.rs
index 79a4ed9960e45e97982cdaa9fc0f6373a4764aed..89cd300b418f64d41c75a8919a41f018b63749c4 100644
--- a/umbrella/src/lib.rs
+++ b/umbrella/src/lib.rs
@@ -328,6 +328,10 @@ pub use pallet_assets;
 #[cfg(feature = "pallet-assets-freezer")]
 pub use pallet_assets_freezer;
 
+/// 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;