diff --git a/pallets/pallet-deposit-storage/src/deposit/mock.rs b/pallets/pallet-deposit-storage/src/deposit/mock.rs index 64599e9a7..3eeb97eb5 100644 --- a/pallets/pallet-deposit-storage/src/deposit/mock.rs +++ b/pallets/pallet-deposit-storage/src/deposit/mock.rs @@ -158,6 +158,14 @@ impl ExtBuilder { self } + pub(crate) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::try_state::(System::block_number()).unwrap(); + }); + } + pub(crate) fn build(self) -> sp_io::TestExternalities { let mut ext = sp_io::TestExternalities::default(); diff --git a/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs b/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs index cecf9ed40..501888721 100644 --- a/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs +++ b/pallets/pallet-deposit-storage/src/deposit/tests/on_commitment_removed.rs @@ -51,8 +51,7 @@ fn on_commitment_removed_successful() { reason: HoldReason::Deposit.into(), }, )]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { assert_eq!( Pallet::::deposits(&namespace, &key), Some(DepositEntry { @@ -103,8 +102,7 @@ fn on_commitment_removed_different_owner_successful() { reason: HoldReason::Deposit.into(), }, )]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { assert_eq!( Pallet::::deposits(&namespace, &key), Some(DepositEntry { @@ -139,7 +137,7 @@ fn on_commitment_removed_different_owner_successful() { #[test] fn on_commitment_removed_deposit_not_found() { - ExtBuilder::default().build().execute_with(|| { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { assert_noop!( as ProviderHooks>::on_commitment_removed( &SUBJECT, diff --git a/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs b/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs index 1103cda7c..932ae10e3 100644 --- a/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs +++ b/pallets/pallet-deposit-storage/src/deposit/tests/on_identity_committed.rs @@ -37,8 +37,7 @@ use crate::{ fn on_identity_committed_successful() { ExtBuilder::default() .with_balances(vec![(SUBMITTER, 100_000)]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { let namespace = DepositNamespaces::get(); let key: DepositKeyOf = (SUBJECT, SUBMITTER, 0 as IdentityCommitmentVersion) .encode() @@ -94,8 +93,7 @@ fn on_identity_committed_existing_deposit() { }, }, )]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { assert_noop!( as ProviderHooks>::on_identity_committed( &SUBJECT, @@ -110,7 +108,7 @@ fn on_identity_committed_existing_deposit() { #[test] fn on_identity_committed_insufficient_balance() { - ExtBuilder::default().build().execute_with(|| { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { assert_noop!( as ProviderHooks>::on_identity_committed( &SUBJECT, diff --git a/pallets/pallet-deposit-storage/src/fungible/mod.rs b/pallets/pallet-deposit-storage/src/fungible/mod.rs new file mode 100644 index 000000000..fa6a53e4e --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/mod.rs @@ -0,0 +1,242 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::{ + fungible::{Dust, Inspect, InspectHold, MutateHold, Unbalanced, UnbalancedHold}, + tokens::{Fortitude, Precision, Preservation, Provenance, WithdrawConsequence}, +}; +use kilt_support::Deposit; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::{ + traits::{CheckedSub, Zero}, + DispatchError, DispatchResult, +}; + +use crate::{deposit::DepositEntry, Config, DepositKeyOf, Error, HoldReason, Pallet, SystemDeposits}; + +const LOG_TARGET: &str = "runtime::pallet_deposit_storage::mutate-hold"; + +#[cfg(test)] +mod tests; + +#[cfg(any(test, feature = "try-runtime"))] +pub(super) mod try_state; + +// This trait is implemented by forwarding everything to the `Currency` +// implementation. +impl Inspect for Pallet +where + T: Config, +{ + type Balance = >::Balance; + + fn total_issuance() -> Self::Balance { + >::total_issuance() + } + + fn minimum_balance() -> Self::Balance { + >::minimum_balance() + } + + fn total_balance(who: &T::AccountId) -> Self::Balance { + >::total_balance(who) + } + + fn balance(who: &T::AccountId) -> Self::Balance { + >::balance(who) + } + + fn reducible_balance(who: &T::AccountId, preservation: Preservation, force: Fortitude) -> Self::Balance { + >::reducible_balance(who, preservation, force) + } + + fn can_deposit( + who: &T::AccountId, + amount: Self::Balance, + provenance: Provenance, + ) -> frame_support::traits::tokens::DepositConsequence { + >::can_deposit(who, amount, provenance) + } + + fn can_withdraw(who: &T::AccountId, amount: Self::Balance) -> WithdrawConsequence { + >::can_withdraw(who, amount) + } +} + +/// The `HoldReason` consumers must use in order to rely on this pallet's +/// implementation of `MutateHold`. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Copy, Debug, Default)] +pub struct PalletDepositStorageReason { + pub(crate) namespace: Namespace, + pub(crate) key: Key, +} + +impl From> for HoldReason { + fn from(_value: PalletDepositStorageReason) -> Self { + // All the deposits ever taken like this will count towards the same hold + // reason. + Self::FungibleImpl + } +} + +impl InspectHold for Pallet +where + T: Config, +{ + type Reason = PalletDepositStorageReason>; + + fn total_balance_on_hold(who: &T::AccountId) -> Self::Balance { + >::total_balance_on_hold(who) + } + + // We return the balance of hold of a specific (namespace, key), which prevents + // mistakes since everything in the end is converted to `HoldReason`. + fn balance_on_hold(reason: &Self::Reason, who: &T::AccountId) -> Self::Balance { + let Some(deposit_entry) = SystemDeposits::::get(&reason.namespace, &reason.key) else { + return Zero::zero(); + }; + if deposit_entry.deposit.owner == *who { + deposit_entry.deposit.amount + } else { + Zero::zero() + } + } +} + +// This trait is implemented by forwarding everything to the `Currency` +// implementation. +impl Unbalanced for Pallet +where + T: Config, +{ + fn handle_dust(dust: Dust) { + >::handle_dust(Dust(dust.0)); + } + + fn write_balance(who: &T::AccountId, amount: Self::Balance) -> Result, DispatchError> { + >::write_balance(who, amount) + } + + fn set_total_issuance(amount: Self::Balance) { + >::set_total_issuance(amount); + } +} + +impl UnbalancedHold for Pallet +where + T: Config, +{ + /// This function gets called inside all invocations of `hold`, `release`, + /// and `release_all`. The value of `amount` is always the final amount that + /// should be kept on hold, meaning that a `release_all` will result in a + /// value of `0`. An hold increase from `1` to `2` will imply a value of + /// `amount` of `2`, i.e., the total amount held at the end of the + /// evaluation. Similarly, a decrease from `3` to `1` will imply a `amount` + /// value of `1`. + fn set_balance_on_hold(reason: &Self::Reason, who: &T::AccountId, amount: Self::Balance) -> DispatchResult { + let runtime_reason = T::RuntimeHoldReason::from(reason.clone().into()); + SystemDeposits::::try_mutate_exists(&reason.namespace, &reason.key, |maybe_existing_deposit_entry| { + match maybe_existing_deposit_entry { + // If this is the first registered deposit for this reason, create a new storage entry. + None => { + if amount > Zero::zero() { + // Increase the held amount for the final runtime hold reason by `amount`. + >::increase_balance_on_hold( + &runtime_reason, + who, + amount, + Precision::Exact, + )?; + *maybe_existing_deposit_entry = Some(DepositEntry { + deposit: Deposit { + amount, + owner: who.clone(), + }, + reason: runtime_reason, + }) + } + Ok(()) + } + Some(existing_deposit_entry) => { + // The pallet assumes each (namespace, key) points to a unique deposit, so the + // same combination cannot be used for a different account. + if existing_deposit_entry.deposit.owner != *who { + return Err(DispatchError::from(Error::::DepositExisting)); + } + if amount.is_zero() { + // If trying to remove all holds for this reason (`amount` == 0), decrease the + // held amount for the final runtime hold reason by the amount that was held by + // this reason. + >::decrease_balance_on_hold( + &runtime_reason, + who, + existing_deposit_entry.deposit.amount, + Precision::Exact, + )?; + // Since we're setting the held amount to `0`, remove the storage entry. + *maybe_existing_deposit_entry = None; + // We're trying to update (i.e., not creating, not deleting) + // the held amount for this hold reason. + } else { + // If trying to increase the amount held, increase the held amount for the final + // runtime hold reason by the (amount - stored amount) difference. + if amount >= existing_deposit_entry.deposit.amount { + let amount_to_hold = amount + .checked_sub(&existing_deposit_entry.deposit.amount) + .ok_or_else(|| { + log::error!(target: LOG_TARGET, "Failed to evaluate {:?} - {:?}", amount, existing_deposit_entry.deposit.amount); + Error::::Internal + })?; + >::increase_balance_on_hold( + &runtime_reason, + who, + amount_to_hold, + Precision::Exact, + )?; + // Else, decrease the held amount for the final runtime + // hold reason by the (stored amount - amount) + // difference. + } else { + let amount_to_release = existing_deposit_entry + .deposit + .amount + .checked_sub(&amount) + .ok_or_else(|| { + log::error!(target: LOG_TARGET, "Failed to evaluate {:?} - {:?}", existing_deposit_entry.deposit.amount, amount); + Error::::Internal + })?; + >::decrease_balance_on_hold( + &runtime_reason, + who, + amount_to_release, + Precision::Exact, + )?; + } + // In either case, update the storage entry with the specified amount. + existing_deposit_entry.deposit.amount = amount; + } + Ok(()) + } + } + })?; + Ok(()) + } +} + +impl MutateHold for Pallet where T: Config {} diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/burn.rs b/pallets/pallet-deposit-storage/src/fungible/tests/burn.rs new file mode 100644 index 000000000..2f67fb953 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/burn.rs @@ -0,0 +1,98 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::{ + fungible::MutateHold, + tokens::{Fortitude, Precision}, +}; +use kilt_support::Deposit; +use sp_runtime::AccountId32; + +use crate::{ + deposit::DepositEntry, + fungible::{ + tests::mock::{ExtBuilder, TestRuntime, TestRuntimeHoldReason, OWNER}, + PalletDepositStorageReason, + }, + Pallet, SystemDeposits, +}; + +#[test] +fn burn_held() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + + as MutateHold>::burn_held( + &reason, + &OWNER, + 9, + Precision::Exact, + Fortitude::Polite, + ) + .expect("Failed to burn partial amount for user."); + let deposit_entry = SystemDeposits::::get(&reason.namespace, &reason.key) + .expect("Deposit entry should not be None."); + assert_eq!( + deposit_entry, + DepositEntry { + deposit: Deposit { + amount: 1, + owner: OWNER + }, + reason: TestRuntimeHoldReason::Deposit, + } + ); + + // Burn the outstanding holds. + as MutateHold>::burn_held( + &reason, + &OWNER, + 1, + Precision::Exact, + Fortitude::Polite, + ) + .expect("Failed to burn remaining amount for user."); + assert!(SystemDeposits::::get(&reason.namespace, &reason.key).is_none()); + }); +} + +#[test] +fn burn_all_held() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + + as MutateHold>::burn_all_held( + &reason, + &OWNER, + Precision::Exact, + Fortitude::Polite, + ) + .expect("Failed to burn all amount for user."); + assert!(SystemDeposits::::get(&reason.namespace, &reason.key).is_none()); + }); +} diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/hold.rs b/pallets/pallet-deposit-storage/src/fungible/tests/hold.rs new file mode 100644 index 000000000..6f06bfe9a --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/hold.rs @@ -0,0 +1,118 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// 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::fungible::MutateHold}; +use kilt_support::Deposit; +use sp_runtime::AccountId32; + +use crate::{ + deposit::DepositEntry, + fungible::{ + tests::mock::{Balances, ExtBuilder, TestRuntime, TestRuntimeHoldReason, OTHER_ACCOUNT, OWNER}, + PalletDepositStorageReason, + }, + Error, Pallet, SystemDeposits, +}; + +#[test] +fn hold() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + let deposit_entry = SystemDeposits::::get(&reason.namespace, &reason.key) + .expect("Deposit entry should not be None."); + assert_eq!( + deposit_entry, + DepositEntry { + deposit: Deposit { + amount: 10, + owner: OWNER + }, + reason: TestRuntimeHoldReason::Deposit, + } + ); + + as MutateHold>::hold(&reason, &OWNER, 5) + .expect("Failed to hold amount for user."); + let deposit_entry = SystemDeposits::::get(&reason.namespace, &reason.key) + .expect("Deposit entry should not be None."); + assert_eq!( + deposit_entry, + DepositEntry { + deposit: Deposit { + amount: 15, + owner: OWNER + }, + reason: TestRuntimeHoldReason::Deposit, + } + ); + }); +} + +#[test] +fn zero_hold() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + as MutateHold>::hold(&reason, &OWNER, 0) + .expect("Failed to hold amount for user."); + // A hold of zero for a new deposit should not create any new storage entry. + assert!(SystemDeposits::::get(&reason.namespace, &reason.key).is_none()); + }); +} + +#[test] +fn hold_same_reason_different_user() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000), (OTHER_ACCOUNT, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + assert_ok!( as MutateHold>::hold( + &reason, &OWNER, 10 + )); + assert_err!( + as MutateHold>::hold(&reason, &OTHER_ACCOUNT, 10), + Error::::DepositExisting + ); + }); +} + +#[test] +fn too_many_holds() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + // Occupy the only hold available with a different reason. + >::hold(&TestRuntimeHoldReason::Else, &OWNER, 1) + .expect("Failed to hold amount for user."); + // Try to hold a second time, hitting the mock limit of 1. + assert_err!( + as MutateHold>::hold( + &PalletDepositStorageReason::default(), + &OWNER, + 10 + ), + pallet_balances::Error::::TooManyHolds + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/inspect_hold.rs b/pallets/pallet-deposit-storage/src/fungible/tests/inspect_hold.rs new file mode 100644 index 000000000..faa5579af --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/inspect_hold.rs @@ -0,0 +1,107 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::{ + fungible::{InspectHold, MutateHold}, + tokens::Precision, +}; +use sp_runtime::{traits::Zero, AccountId32}; + +use crate::{ + fungible::tests::mock::{Balances, DepositNamespace, ExtBuilder, TestRuntime, OTHER_ACCOUNT, OWNER}, + HoldReason, Pallet, PalletDepositStorageReason, +}; + +#[test] +fn balance_on_hold() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason { + namespace: DepositNamespace::ExampleNamespace, + key: [0].to_vec().try_into().unwrap(), + }; + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + + let balance_on_hold_for_same_reason_same_user = + as InspectHold>::balance_on_hold(&reason, &OWNER); + assert_eq!(balance_on_hold_for_same_reason_same_user, 10); + + let balance_on_hold_for_same_reason_different_user = + as InspectHold>::balance_on_hold(&reason, &OTHER_ACCOUNT); + assert!(balance_on_hold_for_same_reason_different_user.is_zero()); + + let other_reason = PalletDepositStorageReason { + namespace: DepositNamespace::ExampleNamespace, + key: [1].to_vec().try_into().unwrap(), + }; + let balance_on_hold_for_different_reason_same_user = + as InspectHold>::balance_on_hold(&other_reason, &OWNER); + assert!(balance_on_hold_for_different_reason_same_user.is_zero()); + + let balance_on_hold_for_different_reason_different_user = + as InspectHold>::balance_on_hold(&other_reason, &OTHER_ACCOUNT); + assert!(balance_on_hold_for_different_reason_different_user.is_zero()); + }); +} + +#[test] +fn multiple_holds() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason { + namespace: DepositNamespace::ExampleNamespace, + key: [0].to_vec().try_into().unwrap(), + }; + let other_reason = PalletDepositStorageReason { + namespace: DepositNamespace::ExampleNamespace, + key: [1].to_vec().try_into().unwrap(), + }; + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + // The two different reasons should be stored under the same runtime reason in + // the underlying `Currency`. + assert_eq!( + >::balance_on_hold(&HoldReason::Deposit.into(), &OWNER), + 10 + ); + + as MutateHold>::hold(&other_reason, &OWNER, 15) + .expect("Failed to hold amount for user."); + assert_eq!( + >::balance_on_hold(&HoldReason::Deposit.into(), &OWNER), + 25 + ); + + as MutateHold>::release(&other_reason, &OWNER, 15, Precision::Exact) + .expect("Failed to release amount for user."); + assert_eq!( + >::balance_on_hold(&HoldReason::Deposit.into(), &OWNER), + 10 + ); + + as MutateHold>::release(&reason, &OWNER, 10, Precision::Exact) + .expect("Failed to release amount for user."); + assert!( + >::balance_on_hold(&HoldReason::Deposit.into(), &OWNER).is_zero() + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/mock.rs b/pallets/pallet-deposit-storage/src/fungible/tests/mock.rs new file mode 100644 index 000000000..9ef33876c --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/mock.rs @@ -0,0 +1,159 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + construct_runtime, + sp_runtime::{ + testing::H256, + traits::{BlakeTwo256, IdentityLookup}, + AccountId32, + }, + traits::{ConstU128, ConstU16, ConstU32, ConstU64, Currency, Everything, VariantCount}, +}; +use frame_system::{mocking::MockBlock, EnsureSigned}; +use parity_scale_codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_runtime::RuntimeDebug; + +use crate::{self as storage_deposit_pallet, HoldReason}; + +construct_runtime!( + pub struct TestRuntime { + System: frame_system, + StorageDepositPallet: storage_deposit_pallet, + Balances: pallet_balances, + } +); + +pub(crate) type Balance = u128; + +// Required to test more than a single hold reason without introducing any new +// pallets. +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub enum TestRuntimeHoldReason { + Deposit, + Else, +} + +impl From for TestRuntimeHoldReason { + fn from(_value: HoldReason) -> Self { + Self::Deposit + } +} + +// This value is used by the `Balances` pallet to create the limit for the +// `BoundedVec` of holds. By returning `1` here, we make it possible to hit to +// hold limit. +impl VariantCount for TestRuntimeHoldReason { + const VARIANT_COUNT: u32 = 1; +} + +impl frame_system::Config for TestRuntime { + type AccountData = pallet_balances::AccountData; + type AccountId = AccountId32; + type BaseCallFilter = Everything; + type Block = MockBlock; + type BlockHashCount = ConstU64<256>; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type RuntimeTask = (); + type SS58Prefix = ConstU16<1>; + type SystemWeightInfo = (); + type Version = (); +} + +impl pallet_balances::Config for TestRuntime { + type RuntimeFreezeReason = RuntimeFreezeReason; + type FreezeIdentifier = RuntimeFreezeReason; + type RuntimeHoldReason = TestRuntimeHoldReason; + type MaxFreezes = ConstU32<1>; + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = ConstU32<1>; + type MaxReserves = ConstU32<1>; + type ReserveIdentifier = [u8; 8]; +} + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, PartialEq, Eq, RuntimeDebug, Default)] +pub enum DepositNamespace { + #[default] + ExampleNamespace, +} + +impl crate::Config for TestRuntime { + type CheckOrigin = EnsureSigned; + type Currency = Balances; + type DepositHooks = (); + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = TestRuntimeHoldReason; + type MaxKeyLength = ConstU32<256>; + type Namespace = DepositNamespace; + #[cfg(feature = "runtime-benchmarks")] + type BenchmarkHooks = (); + type WeightInfo = (); +} + +pub(crate) const OWNER: AccountId32 = AccountId32::new([100u8; 32]); +pub(crate) const OTHER_ACCOUNT: AccountId32 = AccountId32::new([101u8; 32]); + +#[derive(Default)] +pub(crate) struct ExtBuilder(Vec<(AccountId32, Balance)>); + +impl ExtBuilder { + pub(crate) fn with_balances(mut self, balances: Vec<(AccountId32, Balance)>) -> Self { + self.0 = balances; + self + } + + pub(crate) fn build(self) -> sp_io::TestExternalities { + let mut ext = sp_io::TestExternalities::default(); + + ext.execute_with(|| { + for (account_id, amount) in self.0 { + Balances::make_free_balance_be(&account_id, amount); + } + }); + + ext + } + + pub(crate) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::try_state::(System::block_number()).unwrap(); + }); + } +} diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/mod.rs b/pallets/pallet-deposit-storage/src/fungible/tests/mod.rs new file mode 100644 index 000000000..253140420 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/mod.rs @@ -0,0 +1,24 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +mod burn; +mod hold; +mod inspect_hold; +mod mock; +mod release; +mod transfer; diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/release.rs b/pallets/pallet-deposit-storage/src/fungible/tests/release.rs new file mode 100644 index 000000000..bc49b5d01 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/release.rs @@ -0,0 +1,104 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ + assert_err, + traits::{fungible::MutateHold, tokens::Precision}, +}; +use kilt_support::Deposit; +use sp_runtime::{AccountId32, TokenError}; + +use crate::{ + deposit::DepositEntry, + fungible::{ + tests::mock::{DepositNamespace, ExtBuilder, TestRuntime, TestRuntimeHoldReason, OWNER}, + PalletDepositStorageReason, + }, + Pallet, SystemDeposits, +}; + +#[test] +fn release() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + + as MutateHold>::release(&reason, &OWNER, 5, Precision::Exact) + .expect("Failed to release partial amount for user."); + let deposit_entry = SystemDeposits::::get(&reason.namespace, &reason.key) + .expect("Deposit entry should not be None."); + assert_eq!( + deposit_entry, + DepositEntry { + deposit: Deposit { + amount: 5, + owner: OWNER + }, + reason: TestRuntimeHoldReason::Deposit, + } + ); + + // Remove the outstanding holds. + as MutateHold>::release(&reason, &OWNER, 5, Precision::Exact) + .expect("Failed to release remaining amount for user."); + assert!(SystemDeposits::::get(&reason.namespace, &reason.key).is_none()); + }); +} + +#[test] +fn release_different_reason() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason { + namespace: DepositNamespace::ExampleNamespace, + key: [0].to_vec().try_into().unwrap(), + }; + let other_reason = PalletDepositStorageReason { + namespace: DepositNamespace::ExampleNamespace, + key: [1].to_vec().try_into().unwrap(), + }; + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + assert_err!( + as MutateHold>::release(&other_reason, &OWNER, 10, Precision::Exact), + TokenError::FundsUnavailable + ); + }); +} + +#[test] +fn release_all() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + + as MutateHold>::release_all(&reason, &OWNER, Precision::Exact) + .expect("Failed to release all amount for user."); + assert!(SystemDeposits::::get(&reason.namespace, &reason.key).is_none()); + }); +} diff --git a/pallets/pallet-deposit-storage/src/fungible/tests/transfer.rs b/pallets/pallet-deposit-storage/src/fungible/tests/transfer.rs new file mode 100644 index 000000000..104cfee29 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/tests/transfer.rs @@ -0,0 +1,100 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::traits::{ + fungible::MutateHold, + tokens::{Fortitude, Precision, Preservation, Restriction}, +}; +use kilt_support::Deposit; +use sp_runtime::AccountId32; + +use crate::{ + deposit::DepositEntry, + fungible::{ + tests::mock::{ExtBuilder, TestRuntime, TestRuntimeHoldReason, OTHER_ACCOUNT, OWNER}, + PalletDepositStorageReason, + }, + Pallet, SystemDeposits, +}; + +#[test] +fn transfer_on_hold() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000), (OTHER_ACCOUNT, 1)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::hold(&reason, &OWNER, 10) + .expect("Failed to hold amount for user."); + + as MutateHold>::transfer_on_hold( + &reason, + &OWNER, + &OTHER_ACCOUNT, + 10, + Precision::Exact, + Restriction::OnHold, + Fortitude::Polite, + ) + .expect("Failed to transfer held tokens."); + let deposit_entry = SystemDeposits::::get(&reason.namespace, &reason.key) + .expect("Deposit entry should not be None."); + assert_eq!( + deposit_entry, + DepositEntry { + deposit: Deposit { + amount: 10, + owner: OTHER_ACCOUNT + }, + reason: TestRuntimeHoldReason::Deposit, + } + ); + }); +} + +#[test] +fn transfer_and_hold() { + ExtBuilder::default() + .with_balances(vec![(OWNER, 100_000), (OTHER_ACCOUNT, 1)]) + .build_and_execute_with_sanity_tests(|| { + let reason = PalletDepositStorageReason::default(); + + as MutateHold>::transfer_and_hold( + &reason, + &OWNER, + &OTHER_ACCOUNT, + 10, + Precision::Exact, + Preservation::Preserve, + Fortitude::Polite, + ) + .expect("Failed to transfer free tokens to be held."); + let deposit_entry = SystemDeposits::::get(&reason.namespace, &reason.key) + .expect("Deposit entry should not be None."); + assert_eq!( + deposit_entry, + DepositEntry { + deposit: Deposit { + amount: 10, + owner: OTHER_ACCOUNT + }, + reason: TestRuntimeHoldReason::Deposit, + } + ); + }); +} diff --git a/pallets/pallet-deposit-storage/src/fungible/try_state.rs b/pallets/pallet-deposit-storage/src/fungible/try_state.rs new file mode 100644 index 000000000..9be95b0ec --- /dev/null +++ b/pallets/pallet-deposit-storage/src/fungible/try_state.rs @@ -0,0 +1,71 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +use frame_support::{ensure, traits::fungible::InspectHold}; +use frame_system::pallet_prelude::BlockNumberFor; +use kilt_support::Deposit; +use sp_runtime::{traits::CheckedAdd, TryRuntimeError}; +use sp_std::collections::btree_map::BTreeMap; + +use crate::{deposit::DepositEntry, AccountIdOf, BalanceOf, Config, HoldReason, SystemDeposits}; + +// Verify the state kept as part of the `MutateHold` implementation is +// consistent with the state of the `Currency` type. +pub(crate) fn check_fungible_consistency(_n: BlockNumberFor) -> Result<(), TryRuntimeError> +where + T: Config, +{ + // Sum together all the deposits stored as part of the `MutateHold` + // implementation, and fail if any of them does not have the expected + // `crate::HoldReason::FungibleImpl` reason. + let users_deposits = SystemDeposits::::iter_values().try_fold( + BTreeMap::, BalanceOf>::new(), + |mut sum, + DepositEntry { + reason, + deposit: Deposit { amount, owner }, + }| + -> Result<_, TryRuntimeError> { + ensure!( + reason == HoldReason::FungibleImpl.into(), + TryRuntimeError::Other("Found a deposit reason different than the expected `HoldReason::FungibleImpl`") + ); + + // Fold the deposit amount for the current user. + let entry = sum.entry(owner).or_default(); + *entry = entry + .checked_add(&amount) + .ok_or(TryRuntimeError::Other("Failed to fold deposits for user."))?; + + Ok(sum) + }, + )?; + // We verify that the total balance on hold for the `HoldReason::FungibleImpl` + // matches the amount of deposits stored in this pallet. + users_deposits + .into_iter() + .try_for_each(|(owner, deposit_sum)| -> Result<_, TryRuntimeError> { + ensure!( + >>::balance_on_hold(&HoldReason::FungibleImpl.into(), &owner) + == deposit_sum, + TryRuntimeError::Other("Deposit sum for user different than the expected amount") + ); + Ok(()) + })?; + Ok(()) +} diff --git a/pallets/pallet-deposit-storage/src/lib.rs b/pallets/pallet-deposit-storage/src/lib.rs index 6c3065d25..645cabd25 100644 --- a/pallets/pallet-deposit-storage/src/lib.rs +++ b/pallets/pallet-deposit-storage/src/lib.rs @@ -27,6 +27,8 @@ mod default_weights; mod deposit; +mod fungible; +pub use fungible::PalletDepositStorageReason; pub mod traits; #[cfg(test)] @@ -35,12 +37,15 @@ mod mock; #[cfg(test)] mod tests; +#[cfg(any(test, feature = "try-runtime"))] +mod try_state; + #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub use crate::{default_weights::WeightInfo, deposit::FixedDepositCollectorViaDepositsPallet, pallet::*}; -const LOG_TARGET: &str = "pallet_deposit_storage"; +const LOG_TARGET: &str = "runtime::pallet_deposit_storage"; #[frame_support::pallet] // `.expect()` is used in the macro-generated code, and we have to ignore it. @@ -67,7 +72,7 @@ pub mod pallet { pallet_prelude::*, traits::{ fungible::{hold::Mutate, Inspect}, - EnsureOrigin, + EnsureOrigin, Hooks, }, }; use frame_system::pallet_prelude::*; @@ -113,6 +118,7 @@ pub mod pallet { #[pallet::composite_enum] pub enum HoldReason { Deposit, + FungibleImpl, } #[pallet::error] @@ -132,6 +138,8 @@ pub mod pallet { FailedToRelease, /// The external hook failed. Hook(u16), + /// Internal error. + Internal, } #[pallet::event] @@ -171,10 +179,32 @@ pub mod pallet { DepositEntryOf, >; + /// Storage of all system deposits. They are the same as user deposits, but + /// cannot be claimed back by the payers. Instead, some on chain logic must + /// trigger their release. + #[pallet::storage] + #[pallet::getter(fn system_deposits)] + pub(crate) type SystemDeposits = StorageDoubleMap< + _, + Blake2_128Concat, + ::Namespace, + Blake2_128Concat, + DepositKeyOf, + DepositEntryOf, + >; + #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); + #[pallet::hooks] + impl Hooks> for Pallet { + #[cfg(feature = "try-runtime")] + fn try_state(n: BlockNumberFor) -> Result<(), sp_runtime::TryRuntimeError> { + crate::try_state::try_state::(n) + } + } + #[pallet::call] impl Pallet { /// Reclaim a deposit that was previously taken. If there is no deposit diff --git a/pallets/pallet-deposit-storage/src/mock.rs b/pallets/pallet-deposit-storage/src/mock.rs index 8fd1efd73..2ee2f4382 100644 --- a/pallets/pallet-deposit-storage/src/mock.rs +++ b/pallets/pallet-deposit-storage/src/mock.rs @@ -152,6 +152,14 @@ impl ExtBuilder { ext } + pub(crate) fn build_and_execute_with_sanity_tests(self, run: impl FnOnce()) { + let mut ext = self.build(); + ext.execute_with(|| { + run(); + crate::try_state::try_state::(System::block_number()).unwrap(); + }); + } + #[cfg(feature = "runtime-benchmarks")] pub(crate) fn build_with_keystore(self) -> sp_io::TestExternalities { let mut ext = self.build(); diff --git a/pallets/pallet-deposit-storage/src/tests/add_deposit.rs b/pallets/pallet-deposit-storage/src/tests/add_deposit.rs index 149e6f5a3..321ddf60e 100644 --- a/pallets/pallet-deposit-storage/src/tests/add_deposit.rs +++ b/pallets/pallet-deposit-storage/src/tests/add_deposit.rs @@ -30,8 +30,7 @@ fn add_deposit_new() { ExtBuilder::default() // Deposit amount + existential deposit .with_balances(vec![(OWNER, 500 + 10_000)]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { let deposit = DepositEntryOf:: { reason: HoldReason::Deposit.into(), deposit: Deposit { @@ -69,8 +68,7 @@ fn add_deposit_existing() { let key = DepositKeyOf::::default(); ExtBuilder::default() .with_deposits(vec![(namespace.clone(), key.clone(), deposit.clone())]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { assert_noop!( Pallet::::add_deposit(namespace.clone(), key.clone(), deposit), Error::::DepositExisting @@ -80,7 +78,7 @@ fn add_deposit_existing() { #[test] fn add_deposit_failed_to_hold() { - ExtBuilder::default().build().execute_with(|| { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { let deposit = DepositEntryOf:: { reason: HoldReason::Deposit.into(), deposit: Deposit { diff --git a/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs b/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs index 6ad9afece..82223e316 100644 --- a/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs +++ b/pallets/pallet-deposit-storage/src/tests/reclaim_deposit.rs @@ -39,8 +39,7 @@ fn reclaim_deposit_successful() { let key = DepositKeyOf::::default(); ExtBuilder::default() .with_deposits(vec![(namespace.clone(), key.clone(), deposit)]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { assert!(Pallet::::deposits(&namespace, &key).is_some()); assert_eq!(Balances::balance_on_hold(&HoldReason::Deposit.into(), &OWNER), 10_000); @@ -57,7 +56,7 @@ fn reclaim_deposit_successful() { #[test] fn reclaim_deposit_not_found() { - ExtBuilder::default().build().execute_with(|| { + ExtBuilder::default().build_and_execute_with_sanity_tests(|| { assert_noop!( Pallet::::reclaim_deposit( RawOrigin::Signed(OWNER).into(), @@ -82,8 +81,7 @@ fn reclaim_deposit_unauthorized() { let key = DepositKeyOf::::default(); ExtBuilder::default() .with_deposits(vec![(namespace.clone(), key.clone(), deposit)]) - .build() - .execute_with(|| { + .build_and_execute_with_sanity_tests(|| { assert_noop!( Pallet::::reclaim_deposit( RawOrigin::Signed(OTHER_ACCOUNT).into(), diff --git a/pallets/pallet-deposit-storage/src/try_state.rs b/pallets/pallet-deposit-storage/src/try_state.rs new file mode 100644 index 000000000..0e46366e7 --- /dev/null +++ b/pallets/pallet-deposit-storage/src/try_state.rs @@ -0,0 +1,95 @@ +// KILT Blockchain – https://botlabs.org +// Copyright (C) 2019-2024 BOTLabs GmbH + +// The KILT Blockchain is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// The KILT Blockchain is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// If you feel like getting in touch with us, you can do so at info@botlabs.org + +//! Pallet to store namespaced deposits for the configured `Currency`. It allows +//! the original payer of a deposit to claim it back, triggering a hook to +//! optionally perform related actions somewhere else in the runtime. +//! Each deposit is identified by a namespace and a key. There cannot be two +//! equal keys under the same namespace, but the same key can be present under +//! different namespaces. + +use frame_support::{ensure, traits::fungible::InspectHold}; +use frame_system::pallet_prelude::BlockNumberFor; +use kilt_support::Deposit; +use parity_scale_codec::{Decode, Encode}; +use sp_runtime::{traits::CheckedAdd, TryRuntimeError}; +use sp_std::{collections::btree_map::BTreeMap, vec::Vec}; + +use crate::{deposit::DepositEntry, AccountIdOf, BalanceOf, Config, Deposits, HoldReason}; + +pub(crate) fn try_state(n: BlockNumberFor) -> Result<(), TryRuntimeError> +where + T: Config, +{ + crate::fungible::try_state::check_fungible_consistency::(n)?; + check_regular_deposits_consistency::(n)?; + + Ok(()) +} + +// Verify the state outside of the `MutateHold` implementation does not +// interfere with the `MutateHold` state. +fn check_regular_deposits_consistency(_n: BlockNumberFor) -> Result<(), TryRuntimeError> +where + T: Config, +{ + // Sum together all the deposits stored not part of the `MutateHold` + // implementation, and fail if any of them has the unexpected + // `crate::HoldReason::FungibleImpl` reason. + let users_deposits = Deposits::::iter_values().try_fold( + // We can't use `T::RuntimeHoldReason` as a key because it does not implement `Ord`, so we `.encode()` it here. + BTreeMap::<(AccountIdOf, Vec), BalanceOf>::new(), + |mut sum, + DepositEntry { + reason, + deposit: Deposit { amount, owner }, + }| + -> Result<_, TryRuntimeError> { + // Regular deposits should not interfere with the `MutateHold` implementation + // state. + ensure!( + reason != HoldReason::FungibleImpl.into(), + TryRuntimeError::Other("Found a deposit reason `HoldReason::FungibleImpl`, which is unexpected.") + ); + + // Fold the deposit amount for the current user. + let entry = sum.entry((owner, reason.encode())).or_default(); + *entry = entry + .checked_add(&amount) + .ok_or(TryRuntimeError::Other("Failed to fold deposits for user."))?; + + Ok(sum) + }, + )?; + // We verify that the total balance on hold for each hold reason matches the + // amount of deposits stored in this pallet. + users_deposits.into_iter().try_for_each( + |((owner, encoded_runtime_hold_reason), deposit_sum)| -> Result<_, TryRuntimeError> { + let runtime_hold_reason = T::RuntimeHoldReason::decode(&mut encoded_runtime_hold_reason.as_slice()).or( + Err(TryRuntimeError::Other("Failed to decode stored `RuntimeHoldReason`.")), + )?; + ensure!( + >>::balance_on_hold(&runtime_hold_reason, &owner) + == deposit_sum, + TryRuntimeError::Other("Deposit sum for user different than the expected amount") + ); + Ok(()) + }, + )?; + Ok(()) +}