Repository: KILTprotocol/kilt-node
Pull request #866: fix: only destroy funds on refund if collateral is BelowMinimum
Author: login: rflechtner
Created: 2025-03-03T15:09:41Z | Merged: 2025-03-06T08:20:54Z

## fixes https://github.com/KILTprotocol/ticket/issues/3797

Adopts the recommended resolution and checks for the reason of the `can_deposit` check failing. Funds (in bonded coins) are only destroyed if there is a `BelowMinimum` error, which indicates that the account to which the deposit should be send cannot be created because the amount is too low. This scenario can be tolerated, as this amount will likely have negligible financial value.

Other error cases will not lead to the destruction of funds, which means that in theory a refunding pool may not be able to fully refund because some account does not meet criteria for receiving funds. This can include: 
- The maximum amount of Consumer references is exhausted
- The Provider references counter would overflow
- The collateral currency is frozen

## Checklist:

- [x] I have verified that the code works
  - [x] No panics! (checked arithmetic ops, no indexing `array[3]` use `get(3)`, ...)
- [x] I have verified that the code is easy to understand
  - [ ] If not, I have left a well-balanced amount of inline comments
- [ ] I have [left the code in a better state](https://deviq.com/principles/boy-scout-rule)
- [ ] I have documented the changes (where applicable)
    * Either PR or Ticket to update [the Docs](https://github.com/KILTprotocol/docs)
    * Link the PR/Ticket here



Diff:
diff --git a/pallets/pallet-bonded-coins/src/lib.rs b/pallets/pallet-bonded-coins/src/lib.rs
index 469d02089..d4592b4e7 100644
--- a/pallets/pallet-bonded-coins/src/lib.rs
+++ b/pallets/pallet-bonded-coins/src/lib.rs
@@ -64,7 +64,7 @@ pub mod pallet {
 				Create as CreateFungibles, Destroy as DestroyFungibles, Inspect as InspectFungibles,
 				Mutate as MutateFungibles,
 			},
-			tokens::{Fortitude, Precision as WithdrawalPrecision, Preservation, Provenance},
+			tokens::{DepositConsequence, Fortitude, Precision as WithdrawalPrecision, Preservation, Provenance},
 			AccountTouch,
 		},
 		Hashable, Parameter,
@@ -1119,8 +1119,7 @@ pub mod pallet {
 
 			if amount.is_zero()
 				|| T::Collaterals::can_deposit(pool_details.collateral.clone(), &who, amount, Provenance::Extant)
-					.into_result()
-					.is_err()
+					== DepositConsequence::BelowMinimum
 			{
 				// Funds are burnt but the collateral received is not sufficient to be deposited
 				// to the account. This is tolerated as otherwise we could have edge cases where
diff --git a/pallets/pallet-bonded-coins/src/mock.rs b/pallets/pallet-bonded-coins/src/mock.rs
index 21d1887fb..a78b10ae1 100644
--- a/pallets/pallet-bonded-coins/src/mock.rs
+++ b/pallets/pallet-bonded-coins/src/mock.rs
@@ -58,6 +58,7 @@ pub mod runtime {
 		weights::constants::RocksDbWeight,
 	};
 	use frame_system::{EnsureRoot, EnsureSigned};
+	use pallet_assets::FrozenBalance;
 	use sp_core::U256;
 	use sp_runtime::{
 		traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify},
@@ -69,7 +70,7 @@ pub mod runtime {
 		self as pallet_bonded_coins,
 		traits::NextAssetIds,
 		types::{Locks, PoolStatus},
-		Config, DepositBalanceOf, FungiblesAssetIdOf, PoolDetailsOf,
+		AccountIdOf, Config, DepositBalanceOf, FungiblesAssetIdOf, FungiblesBalanceOf, PoolDetailsOf,
 	};
 
 	pub type Hash = sp_core::H256;
@@ -190,6 +191,28 @@ pub mod runtime {
 		}
 	}
 
+	/// Store freezes for the assets pallet.
+	#[storage_alias]
+	pub type Freezes<Assets: PalletInfoAccess> = StorageDoubleMap<
+		Assets,
+		Blake2_128Concat,
+		FungiblesAssetIdOf<Test>,
+		Blake2_128Concat,
+		AccountIdOf<Test>,
+		FungiblesBalanceOf<Test>,
+		OptionQuery,
+	>;
+
+	pub struct FreezesHook;
+
+	impl FrozenBalance<AssetId, AccountId, Balance> for FreezesHook {
+		fn died(_asset: AssetId, _who: &AccountId) {}
+
+		fn frozen_balance(asset: AssetId, who: &AccountId) -> Option<Balance> {
+			Freezes::<Assets>::get(asset, who)
+		}
+	}
+
 	frame_support::construct_runtime!(
 		pub enum Test
 		{
@@ -279,7 +302,7 @@ pub mod runtime {
 		type Currency = Balances;
 		type Extra = ();
 		type ForceOrigin = EnsureRoot<AccountId>;
-		type Freezer = ();
+		type Freezer = FreezesHook;
 		type MetadataDepositBase = ConstU128<0>;
 		type MetadataDepositPerByte = ConstU128<0>;
 		type RemoveItemsLimit = ConstU32<5>;
@@ -355,6 +378,7 @@ pub mod runtime {
 		//  pool_id, PoolDetails
 		pools: Vec<(AccountId, PoolDetailsOf<Test>)>,
 		collaterals: Vec<AssetId>,
+		freezes: Vec<(AssetId, AccountId, Balance)>,
 	}
 
 	impl ExtBuilder {
@@ -378,6 +402,11 @@ pub mod runtime {
 			self
 		}
 
+		pub(crate) fn with_freezes(mut self, freezes: Vec<(AssetId, AccountId, Balance)>) -> Self {
+			self.freezes = freezes;
+			self
+		}
+
 		pub(crate) fn build(self) -> sp_io::TestExternalities {
 			let mut storage = frame_system::GenesisConfig::<Test>::default().build_storage().unwrap();
 
@@ -448,6 +477,10 @@ pub mod runtime {
 				});
 
 				NextAssetId::<BondingPallet>::set(next_asset_id);
+
+				self.freezes.iter().for_each(|(asset_id, account, amount)| {
+					Freezes::<Assets>::set(asset_id, account, Some(*amount));
+				});
 			});
 
 			ext
diff --git a/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs b/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs
index d94d04446..427e1e010 100644
--- a/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs
+++ b/pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs
@@ -17,9 +17,9 @@
 // If you feel like getting in touch with us, you can do so at info@botlabs.org
 use frame_support::{
 	assert_err, assert_ok,
-	traits::fungibles::{Create, Inspect, Mutate},
+	traits::fungibles::{roles::Inspect as InspectRole, Create, Inspect, Mutate},
 };
-use frame_system::{pallet_prelude::OriginFor, RawOrigin};
+use frame_system::{pallet_prelude::OriginFor, ConsumerLimits, RawOrigin};
 use sp_runtime::TokenError;
 
 use crate::{
@@ -266,6 +266,112 @@ fn refund_below_min_balance() {
 		});
 }
 
+#[test]
+fn refund_account_fails_when_account_blocked() {
+	let pool_details = generate_pool_details(
+		vec![DEFAULT_BONDED_CURRENCY_ID],
+		get_linear_bonding_curve(),
+		false,
+		Some(PoolStatus::Refunding),
+		Some(ACCOUNT_00),
+		None,
+		None,
+		None,
+	);
+	let pool_id: AccountIdOf<Test> = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]);
+
+	ExtBuilder::default()
+		.with_native_balances(vec![(ACCOUNT_01, ONE_HUNDRED_KILT)])
+		.with_pools(vec![(pool_id.clone(), pool_details)])
+		.with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID])
+		.with_bonded_balance(vec![
+			(DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), ONE_HUNDRED_KILT),
+			(DEFAULT_COLLATERAL_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+			(DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+		])
+		.build_and_execute_with_sanity_tests(|| {
+			let origin: OriginFor<Test> = RawOrigin::Signed(ACCOUNT_01).into();
+
+			Assets::block(
+				RawOrigin::Signed(Assets::owner(DEFAULT_COLLATERAL_CURRENCY_ID).unwrap()).into(),
+				DEFAULT_COLLATERAL_CURRENCY_ID,
+				ACCOUNT_01,
+			)
+			.expect("Failed to block account for test");
+
+			// Ensure the refund_account call fails due to failing can_deposit check
+			assert_err!(
+				BondingPallet::refund_account(origin, pool_id.clone(), ACCOUNT_01, 0, 1),
+				TokenError::Blocked
+			);
+		});
+}
+
+#[test]
+fn refund_account_fails_if_account_cannot_be_created() {
+	let pool_details = generate_pool_details(
+		vec![DEFAULT_BONDED_CURRENCY_ID],
+		get_linear_bonding_curve(),
+		false,
+		Some(PoolStatus::Refunding),
+		Some(ACCOUNT_00),
+		None,
+		None,
+		None,
+	);
+	let pool_id: AccountIdOf<Test> = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]);
+
+	// get MaxConsumers value
+	let max_consumers: usize = <Test as frame_system::Config>::MaxConsumers::max_consumers()
+		.try_into()
+		.expect("");
+	// Collateral must be non-sufficient for these tests to work, so we'll create a
+	// new collateral asset in the test
+	let collateral_id = DEFAULT_BONDED_CURRENCY_ID + 1;
+
+	ExtBuilder::default()
+		.with_native_balances(vec![
+			(ACCOUNT_01, ONE_HUNDRED_KILT),
+			(pool_id.clone(), ONE_HUNDRED_KILT),
+		])
+		.with_pools(vec![(pool_id.clone(), pool_details)])
+		.with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID])
+		.with_bonded_balance(vec![
+			(DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), ONE_HUNDRED_KILT),
+			(DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+		])
+		// Freeze some funds for ACCOUNT_01 so refund can only be partial
+		.with_freezes(vec![(DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, 100_000)])
+		.build_and_execute_with_sanity_tests(|| {
+			// switch pool's collateral to a non-sufficient one
+			assert_ok!(<Assets as Create<_>>::create(collateral_id, ACCOUNT_00, false, 1));
+			Pools::<Test>::mutate(&pool_id, |details| details.as_mut().unwrap().collateral = collateral_id);
+			// make sure pool account holds collateral - this should work because it also
+			// holds the (sufficient) original collateral
+			assert_ok!(Assets::mint_into(collateral_id, &pool_id, ONE_HUNDRED_KILT));
+			// create non-sufficient assets to increase ACCOUNT_01 consumers count
+			// the assets pallet requires at least two references to be available for
+			// creating an account, but creates only one, meaning we create max_consumers -
+			// 2 currencies (resulting in max_consumers - 1 consumers because we created one
+			// previously)
+			for i in 1..max_consumers - 1 {
+				let i_u32: u32 = i.try_into().expect("Failed to convert to u32");
+				let asset_id = collateral_id + i_u32;
+				assert_ok!(<Assets as Create<_>>::create(asset_id, ACCOUNT_00, false, 1));
+				assert_ok!(Assets::mint_into(asset_id, &ACCOUNT_01, ONE_HUNDRED_KILT));
+			}
+
+			let origin: OriginFor<Test> = RawOrigin::Signed(ACCOUNT_01).into();
+
+			// Collateral is non-sufficient and thus would create additional consumer
+			// references on ACCOUNT_01, which is too many
+			assert_err!(
+				BondingPallet::refund_account(origin, pool_id.clone(), ACCOUNT_01, 0, 1),
+				TokenError::CannotCreate
+			);
+		});
+}
+
 #[test]
 fn refund_account_fails_when_pool_not_refunding() {
 	let pool_details = generate_pool_details(


Reviews:
user: login: abdulmth
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:28:21Z
user: login: rflechtner
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T10:32:10Z
user: login: rflechtner
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-04T13:34:28Z
user: login: ntn-x2
body: The PR addresses the problem you mention, although I am not sure why we would treat only one case as a special case for burning funds? I believe this might result in an unpleasant UX. It would see the error being ignored perhaps in a `force_` extrinsic, also because this would mean that a user would pay for tx fees to end up with nothing, so if they know beforehand, they have no incentive to call this extrinsic in the first place.
Other than this, if we're still going down this route, I think one more test case which covers the only case in which the error should be ignored (i.e., `BelowMinimum`) should be added. Ideally, we would have one test case for each of the error variants, to make sure that we catch changes in case we modify those bits of code.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T07:45:10Z
user: login: rflechtner
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:08:45Z
user: login: ntn-x2
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T14:00:28Z

Review comments:
diff_hunk: @@ -1119,8 +1119,7 @@ pub mod pallet {
 
 			if amount.is_zero()
 				|| T::Collaterals::can_deposit(pool_details.collateral.clone(), &who, amount, Provenance::Extant)
-					.into_result()
-					.is_err()
+					== DepositConsequence::BelowMinimum
path: pallets/pallet-bonded-coins/src/lib.rs
user: login: abdulmth
body: in what case `into_result().is_err()` would be `true`? 🤔 
created_at: 2025-03-04T10:28:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1119,8 +1119,7 @@ pub mod pallet {
 
 			if amount.is_zero()
 				|| T::Collaterals::can_deposit(pool_details.collateral.clone(), &who, amount, Provenance::Extant)
-					.into_result()
-					.is_err()
+					== DepositConsequence::BelowMinimum
path: pallets/pallet-bonded-coins/src/lib.rs
user: login: rflechtner
body: The DepositConsequence has multiple error cases and one success case. In all cases but the success case this would have been false, only the success case yields true. In other words, the transfer below can now still fail (because there's error cases we are not covering in this branch), leading to the extrinsic failing and being rolled back. As I explained in the PR description though, that would be intentional. 
created_at: 2025-03-04T10:32:10Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +266,47 @@ fn refund_below_min_balance() {
 		});
 }
 
+#[test]
+fn refund_account_fails_when_account_blocked() {
path: pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs
user: login: rflechtner
body: @abdulmth We have a test that checks the minimum balance case above this one, and I added a test here that ensures no balance is burnt when the beneficiary account cannot receive the funds due to any other reason (here: being blocked). This test fails if you revert the changes I made to the refund_account extrinsic.
created_at: 2025-03-04T13:34:28Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +266,47 @@ fn refund_below_min_balance() {
 		});
 }
 
+#[test]
+fn refund_account_fails_when_account_blocked() {
+	let pool_details = generate_pool_details(
+		vec![DEFAULT_BONDED_CURRENCY_ID],
+		get_linear_bonding_curve(),
+		false,
+		Some(PoolStatus::Refunding),
+		Some(ACCOUNT_00),
+		None,
+		None,
+		None,
+	);
+	let pool_id: AccountIdOf<Test> = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]);
+
+	ExtBuilder::default()
+		.with_native_balances(vec![(ACCOUNT_01, ONE_HUNDRED_KILT)])
+		.with_pools(vec![(pool_id.clone(), pool_details)])
+		.with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID])
+		.with_bonded_balance(vec![
+			(DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), ONE_HUNDRED_KILT),
+			(DEFAULT_COLLATERAL_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+			(DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+		])
+		.build_and_execute_with_sanity_tests(|| {
+			let origin: OriginFor<Test> = RawOrigin::Signed(ACCOUNT_01).into();
+
+			Assets::block(
+				RawOrigin::Signed(Assets::owner(DEFAULT_COLLATERAL_CURRENCY_ID).unwrap()).into(),
+				DEFAULT_COLLATERAL_CURRENCY_ID,
+				ACCOUNT_01,
+			)
+			.expect("Failed to block account for test");
+
+			// Ensure the refund_account call fails due to pool not being in refunding state
path: pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs
user: login: ntn-x2
body: I guess this comment is outdated, right? This test tests something else. 
created_at: 2025-03-05T07:40:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -266,6 +266,47 @@ fn refund_below_min_balance() {
 		});
 }
 
+#[test]
+fn refund_account_fails_when_account_blocked() {
+	let pool_details = generate_pool_details(
+		vec![DEFAULT_BONDED_CURRENCY_ID],
+		get_linear_bonding_curve(),
+		false,
+		Some(PoolStatus::Refunding),
+		Some(ACCOUNT_00),
+		None,
+		None,
+		None,
+	);
+	let pool_id: AccountIdOf<Test> = calculate_pool_id(&[DEFAULT_BONDED_CURRENCY_ID]);
+
+	ExtBuilder::default()
+		.with_native_balances(vec![(ACCOUNT_01, ONE_HUNDRED_KILT)])
+		.with_pools(vec![(pool_id.clone(), pool_details)])
+		.with_collaterals(vec![DEFAULT_COLLATERAL_CURRENCY_ID])
+		.with_bonded_balance(vec![
+			(DEFAULT_COLLATERAL_CURRENCY_ID, pool_id.clone(), ONE_HUNDRED_KILT),
+			(DEFAULT_COLLATERAL_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+			(DEFAULT_BONDED_CURRENCY_ID, ACCOUNT_01, ONE_HUNDRED_KILT),
+		])
+		.build_and_execute_with_sanity_tests(|| {
+			let origin: OriginFor<Test> = RawOrigin::Signed(ACCOUNT_01).into();
+
+			Assets::block(
+				RawOrigin::Signed(Assets::owner(DEFAULT_COLLATERAL_CURRENCY_ID).unwrap()).into(),
+				DEFAULT_COLLATERAL_CURRENCY_ID,
+				ACCOUNT_01,
+			)
+			.expect("Failed to block account for test");
+
+			// Ensure the refund_account call fails due to pool not being in refunding state
path: pallets/pallet-bonded-coins/src/tests/transactions/refund_account.rs
user: login: rflechtner
body: Good catch, I started with a copy of another test case 4e315967d15219f13cb2d3496eee769d42ca987f
created_at: 2025-03-05T09:08:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: rflechtner
created_at: 2025-03-05T09:48:58Z
body: > The PR addresses the problem you mention, although I am not sure why we would treat only one case as a special case for burning funds? I believe this might result in an unpleasant UX. 

The early return was meant ensure that refunding can always be completed. Knowing now that this could lead to loss of significant amounts of funds we need to change it, meaning that in some scenarios the refund process can be stuck until governance steps in. 

We can accept this for all cases, as you said, treating all cases the same way. I do see it as a likely scenario though that refund amounts for some users are below minimum balance, and it's clear in these cases that the financial loss would be minimal, so IMO it's okay to make an exception here. Also I don't really see how governance would fix this (besides also just burning the funds).

So as far as UX goes, I think it's better to prioritise avoiding refunds getting stuck all the time over minuscule losses of funds.

> It would see the error being ignored perhaps in a `force_` extrinsic, also because this would mean that a user would pay for tx fees to end up with nothing, so if they know beforehand, they have no incentive to call this extrinsic in the first place.

A force extrinsic would mean governance has to step in again, and in this case they could just burn funds directly via the assets pallet I think. The refund_account extrinsic is permissionless though, so anyone can call it for any account (e.g., the pool owner who wants to tear it down).

> Other than this, if we're still going down this route, I think one more test case which covers the only case in which the error should be ignored (i.e., `BelowMinimum`) should be added. Ideally, we would have one test case for each of the error variants, to make sure that we catch changes in case we modify those bits of code.

There's already a test covering the BelowMinimum case, as mentioned in https://github.com/KILTprotocol/kilt-node/pull/866#discussion_r1979443598. Conditions for two other cases are a quite absurd for our case (UnknownAsset -> asset doesn't exist; Overflow -> the balance type or the sufficients counter on the account overflows), but I could add a test for the remaining case (CannotCreate -> too many consumers or no sufficients)


author_association: CONTRIBUTOR
user: login: ntn-x2
created_at: 2025-03-05T09:59:53Z
body: Ok, your points make sense overall, although the amounts that could be burned would be directly related to the ED, so in the case of high ED (for any reason), the amount that could be burned would also be the same. But this is ok and not something we can fix anyway, as you say. Also if the extrinsic is permissionless then that solves the issues of users not willing to call the extrinsic to "do a favor" to the pool owner, so the pool owner can do so themselves.
Regarding tests, yes one test for any other case that makes sense would be good to have IMO.
author_association: CONTRIBUTOR
user: login: rflechtner
created_at: 2025-03-05T10:26:50Z
body: > Ok, your points make sense overall, although the amounts that could be burned would be directly related to the ED, so in the case of high ED (for any reason), the amount that could be burned would also be the same. But this is ok and not something we can fix anyway, as you say. Also if the extrinsic is permissionless then that solves the issues of users not willing to call the extrinsic to "do a favor" to the pool owner, so the pool owner can do so themselves.

Perfect, I'll continue as-is then!

> Regarding tests, yes one test for any other case that makes sense would be good to have IMO.

I'm on it, just a heads-up that these cases only happen when the collateral is non-sufficient, which means I'm going to have to work around the assumption that exists at least for the use of the pallet in our chain that all collaterals are sufficient.


author_association: CONTRIBUTOR
user: login: ntn-x2
created_at: 2025-03-05T10:30:20Z
body: > I'm on it, just a heads-up that these cases only happen when the collateral is non-sufficient, which means I'm going to have to work around the assumption that exists at least for the use of the pallet in our chain that all collaterals are sufficient.

If the assumption is not baked into the pallet, then it is something we need to test, as it is runtime-dependent. If it is, then we can assume it won't happen hence we don't need to test.
author_association: CONTRIBUTOR
user: login: rflechtner
created_at: 2025-03-05T11:42:20Z
body: > If the assumption is not baked into the pallet, then it is something we need to test, as it is runtime-dependent. If it is, then we can assume it won't happen hence we don't need to test.

Turns out this is quite tricky to test. The only cases I could identify where this would happen are:
1. The collateral is non-sufficient but the bonded asset is sufficient. The pallet creates all bonded assets as non-sufficient, so this is a contrived, strange tests case (unless we change this behaviour, see https://github.com/KILTprotocol/ticket/issues/3798).
2. The collateral is non-sufficient and there is a hold on the account for the (also non-sufficient) bonded token, which means the refund is only partial (the free balance is refunded). But Holds/Freezes are implemented in the assets pallet only via a hook, which we don't use in the mocks, so not sure if and how I should implement a hold just for this test.

Should we skip this test for now and make a note to add this later? We're a bit pressed for time with the deadline on addressing the audit issues.
author_association: CONTRIBUTOR
user: login: ntn-x2
created_at: 2025-03-05T11:45:47Z
body: You can skip it for now if you are sure that no weird stuff happens in any combination of cases you mentioned above, as they are all theoretically possible if the pallet is deployed on other runtimes. If you are unsure, and still want to skip the tests, just add a comment where relevant so in case anyone else decides to deploy the pallet, they must ensure the invariants you explain are upheld. What do you think?
author_association: CONTRIBUTOR
user: login: rflechtner
created_at: 2025-03-05T13:46:29Z
body: > You can skip it for now if you are sure that no weird stuff happens in any combination of cases you mentioned above, as they are all theoretically possible if the pallet is deployed on other runtimes. If you are unsure, and still want to skip the tests, just add a comment where relevant so in case anyone else decides to deploy the pallet, they must ensure the invariants you explain are upheld. What do you think?

Ok I managed to make sure the case where the MaxConsumers limit leads to failure to transfer funds is covered: 87cb2e8496d58a43c4ada089477bb22882e1a4d8

This is the only case that is theoretically possible, unless we decide to change the current behaviour where bonded currencies are created as non-sufficient assets. From my POV this should be enough for this PR.
author_association: CONTRIBUTOR