diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_foreign.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_foreign.rs index 5148edb0ee9e2c5f97c9c0afe830e0f87f2ac92f..c76c1137335a17938e5b409c1642a09ce9824fcd 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_foreign.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_foreign.rs @@ -531,4 +531,14 @@ impl<T: frame_system::Config> pallet_assets::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_local.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_local.rs index 4ee235830aed23f9b6c6743a90aa5095bf6f9ed9..cf4f60042bc683fa3a161bbf6d0b9695a718b720 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_local.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_local.rs @@ -528,4 +528,14 @@ impl<T: frame_system::Config> pallet_assets::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_pool.rs b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_pool.rs index df7ad2c633867aaf5fea7c737ed926cccb5e2c80..2cd85de0098952d14e15f9e362e6fbe48d2741e7 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_pool.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-rococo/src/weights/pallet_assets_pool.rs @@ -528,4 +528,14 @@ impl<T: frame_system::Config> pallet_assets::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_foreign.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_foreign.rs index 52ba2fd6c40fcfea552b181bcb05cfecab75f6b4..2692de9aeb50daf5085cba094406b63d6a95c829 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_foreign.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_foreign.rs @@ -537,4 +537,14 @@ impl<T: frame_system::Config> pallet_assets::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_local.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_local.rs index e78366b91cbe8100c2a46c47a8708c3f1aed918d..d2e12549a45c705882a5233d1e4a0825ada8b1eb 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_local.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_local.rs @@ -535,4 +535,14 @@ impl<T: frame_system::Config> pallet_assets::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } diff --git a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_pool.rs b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_pool.rs index 65cae81069c40f7369d31d8411bf882521b71e11..8368f6e583ccf147a7fb9cf0eb9b9ba04b98da3b 100644 --- a/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_pool.rs +++ b/cumulus/parachains/runtimes/assets/asset-hub-westend/src/weights/pallet_assets_pool.rs @@ -529,4 +529,14 @@ impl<T: frame_system::Config> pallet_assets::WeightInfo for WeightInfo<T> { .saturating_add(T::DbWeight::get().reads(2)) .saturating_add(T::DbWeight::get().writes(1)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } diff --git a/prdoc/pr_4527.prdoc b/prdoc/pr_4527.prdoc new file mode 100644 index 0000000000000000000000000000000000000000..12056f87575b8a6d6a01a7e8727f05d25217a364 --- /dev/null +++ b/prdoc/pr_4527.prdoc @@ -0,0 +1,21 @@ +# Schema: Polkadot SDK PRDoc Schema (prdoc) v1.0.0 +# See doc at https://raw.githubusercontent.com/paritytech/polkadot-sdk/master/prdoc/schema_user.json + +title: Call implementation for `transfer_all` + +doc: + - audience: Runtime Dev + description: | + This PR introduces the `transfer_all` call for `pallet-assets`. + The parameters are analog to the same call in `pallet-balances`. + This change is expected to be backwards-compatible. + This change requires running benchmarkings to set accurate weights for + the call. + +crates: + - name: pallet-assets + bump: major + - name: asset-hub-rococo-runtime + bump: minor + - name: asset-hub-westend-runtime + bump: minor diff --git a/substrate/frame/assets/src/benchmarking.rs b/substrate/frame/assets/src/benchmarking.rs index 97cc04174a0c63d630ddfe0b2ebf669c85af56c2..8988323c19851ec3ba058c1fa0ebda3c5524d06c 100644 --- a/substrate/frame/assets/src/benchmarking.rs +++ b/substrate/frame/assets/src/benchmarking.rs @@ -31,6 +31,7 @@ use sp_runtime::traits::Bounded; use crate::Pallet as Assets; const SEED: u32 = 0; +const MIN_BALANCE: u32 = 1; fn default_asset_id<T: Config<I>, I: 'static>() -> T::AssetIdParameter { T::BenchmarkHelper::create_asset_id_parameter(0) @@ -48,7 +49,7 @@ fn create_default_asset<T: Config<I>, I: 'static>( asset_id.clone(), caller_lookup.clone(), is_sufficient, - 1u32.into(), + MIN_BALANCE.into(), ) .is_ok()); (asset_id, caller, caller_lookup) @@ -553,5 +554,15 @@ benchmarks_instance_pallet! { assert_last_event::<T, I>(Event::Blocked { asset_id: asset_id.into(), who: caller }.into()); } + transfer_all { + let amount = T::Balance::from(2 * MIN_BALANCE); + let (asset_id, caller, caller_lookup) = create_default_minted_asset::<T, I>(true, amount); + let target: T::AccountId = account("target", 0, SEED); + let target_lookup = T::Lookup::unlookup(target.clone()); + }: _(SystemOrigin::Signed(caller.clone()), asset_id.clone(), target_lookup, false) + verify { + assert_last_event::<T, I>(Event::Transferred { asset_id: asset_id.into(), from: caller, to: target, amount }.into()); + } + impl_benchmark_test_suite!(Assets, crate::mock::new_test_ext(), crate::mock::Test) } diff --git a/substrate/frame/assets/src/lib.rs b/substrate/frame/assets/src/lib.rs index b9b5b2388dfbe270508b713c315e8a029a27d92f..e9b9a7b1e3cc7b8026920727bad8542d199af21e 100644 --- a/substrate/frame/assets/src/lib.rs +++ b/substrate/frame/assets/src/lib.rs @@ -183,7 +183,11 @@ use frame_support::{ pallet_prelude::DispatchResultWithPostInfo, storage::KeyPrefixIterator, traits::{ - tokens::{fungibles, DepositConsequence, WithdrawConsequence}, + tokens::{ + fungibles, DepositConsequence, Fortitude, + Preservation::{Expendable, Preserve}, + WithdrawConsequence, + }, BalanceStatus::Reserved, Currency, EnsureOriginWithArg, Incrementable, ReservableCurrency, StoredMap, }, @@ -1753,6 +1757,49 @@ pub mod pallet { Self::deposit_event(Event::<T, I>::Blocked { asset_id: id, who }); Ok(()) } + + /// Transfer the entire transferable balance from the caller asset account. + /// + /// NOTE: This function only attempts to transfer _transferable_ balances. This means that + /// any held, frozen, or minimum balance (when `keep_alive` is `true`), will not be + /// transferred by this function. To ensure that this function results in a killed account, + /// you might need to prepare the account by removing any reference counters, storage + /// deposits, etc... + /// + /// The dispatch origin of this call must be Signed. + /// + /// - `id`: The identifier of the asset for the account holding a deposit. + /// - `dest`: The recipient of the transfer. + /// - `keep_alive`: A boolean to determine if the `transfer_all` operation should send all + /// of the funds the asset account has, causing the sender asset account to be killed + /// (false), or transfer everything except at least the minimum balance, which will + /// guarantee to keep the sender asset account alive (true). + #[pallet::call_index(32)] + #[pallet::weight(T::WeightInfo::refund_other())] + pub fn transfer_all( + origin: OriginFor<T>, + id: T::AssetIdParameter, + dest: AccountIdLookupOf<T>, + keep_alive: bool, + ) -> DispatchResult { + let transactor = ensure_signed(origin)?; + let keep_alive = if keep_alive { Preserve } else { Expendable }; + let reducible_balance = <Self as fungibles::Inspect<_>>::reducible_balance( + id.clone().into(), + &transactor, + keep_alive, + Fortitude::Polite, + ); + let dest = T::Lookup::lookup(dest)?; + <Self as fungibles::Mutate<_>>::transfer( + id.into(), + &transactor, + &dest, + reducible_balance, + keep_alive, + )?; + Ok(()) + } } /// Implements [`AccountTouch`] trait. diff --git a/substrate/frame/assets/src/tests.rs b/substrate/frame/assets/src/tests.rs index c751fbdcaf1bb116bf2ecf47dc88b3e33c74bf12..af605c5a3c6403a60c5a768b754c1e900a875083 100644 --- a/substrate/frame/assets/src/tests.rs +++ b/substrate/frame/assets/src/tests.rs @@ -799,6 +799,49 @@ fn transferring_to_blocked_account_should_not_work() { }); } +#[test] +fn transfer_all_works_1() { + new_test_ext().execute_with(|| { + // setup + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 0, true, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(0), 0, 1, 200)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(0), 0, 2, 100)); + // transfer all and allow death + assert_ok!(Assets::transfer_all(Some(1).into(), 0, 2, false)); + assert_eq!(Assets::balance(0, &1), 0); + assert_eq!(Assets::balance(0, &2), 300); + }); +} + +#[test] +fn transfer_all_works_2() { + new_test_ext().execute_with(|| { + // setup + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 0, true, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(0), 0, 1, 200)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(0), 0, 2, 100)); + // transfer all and allow death + assert_ok!(Assets::transfer_all(Some(1).into(), 0, 2, true)); + assert_eq!(Assets::balance(0, &1), 100); + assert_eq!(Assets::balance(0, &2), 200); + }); +} + +#[test] +fn transfer_all_works_3() { + new_test_ext().execute_with(|| { + // setup + assert_ok!(Assets::force_create(RuntimeOrigin::root(), 0, 0, true, 100)); + assert_ok!(Assets::mint(RuntimeOrigin::signed(0), 0, 1, 210)); + set_frozen_balance(0, 1, 10); + 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); + }); +} + #[test] fn origin_guards_should_work() { new_test_ext().execute_with(|| { diff --git a/substrate/frame/assets/src/weights.rs b/substrate/frame/assets/src/weights.rs index 7886cd364d516b71840cc3f95bfcc2299b7c5d18..57f7e951b73c4567a1f37195cedddaa216454f14 100644 --- a/substrate/frame/assets/src/weights.rs +++ b/substrate/frame/assets/src/weights.rs @@ -83,6 +83,7 @@ pub trait WeightInfo { fn refund() -> Weight; fn refund_other() -> Weight; fn block() -> Weight; + fn transfer_all() -> Weight; } /// Weights for `pallet_assets` using the Substrate node and recommended hardware. @@ -530,6 +531,16 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> { .saturating_add(T::DbWeight::get().reads(2_u64)) .saturating_add(T::DbWeight::get().writes(1_u64)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } } // For backwards compatibility and tests. @@ -976,4 +987,14 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().reads(2_u64)) .saturating_add(RocksDbWeight::get().writes(1_u64)) } + + fn transfer_all() -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `3593` + // Minimum execution time: 46_573_000 picoseconds. + Weight::from_parts(47_385_000, 3593) + .saturating_add(RocksDbWeight::get().reads(1_u64)) + .saturating_add(RocksDbWeight::get().writes(1_u64)) + } }