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;