referrerpolicy=no-referrer-when-downgrade

pallet_tips/migrations/
unreserve_deposits.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! A migration that unreserves all deposit and unlocks all stake held in the context of this
19//! pallet.
20
21use alloc::collections::btree_map::BTreeMap;
22use core::iter::Sum;
23use frame_support::{
24	pallet_prelude::OptionQuery,
25	storage_alias,
26	traits::{Currency, LockableCurrency, OnRuntimeUpgrade, ReservableCurrency},
27	weights::RuntimeDbWeight,
28	Parameter, Twox64Concat,
29};
30use sp_runtime::{traits::Zero, Saturating};
31
32#[cfg(feature = "try-runtime")]
33const LOG_TARGET: &str = "runtime::tips::migrations::unreserve_deposits";
34
35type BalanceOf<T, I> =
36	<<T as UnlockConfig<I>>::Currency as Currency<<T as UnlockConfig<I>>::AccountId>>::Balance;
37
38/// The configuration for [`UnreserveDeposits`].
39pub trait UnlockConfig<I>: 'static {
40	/// The hash used in the runtime.
41	type Hash: Parameter;
42	/// The account ID used in the runtime.
43	type AccountId: Parameter + Ord;
44	/// The currency type used in the runtime.
45	///
46	/// Should match the currency type previously used for the pallet, if applicable.
47	type Currency: LockableCurrency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
48	/// Base deposit to report a tip.
49	///
50	/// Should match the currency type previously used for the pallet, if applicable.
51	type TipReportDepositBase: sp_core::Get<BalanceOf<Self, I>>;
52	/// Deposit per byte to report a tip.
53	///
54	/// Should match the currency type previously used for the pallet, if applicable.
55	type DataDepositPerByte: sp_core::Get<BalanceOf<Self, I>>;
56	/// The name of the pallet as previously configured in
57	/// [`construct_runtime!`](frame_support::construct_runtime).
58	type PalletName: sp_core::Get<&'static str>;
59	/// The DB weight as configured in the runtime to calculate the correct weight.
60	type DbWeight: sp_core::Get<RuntimeDbWeight>;
61	/// The block number as configured in the runtime.
62	type BlockNumber: Parameter + Zero + Copy + Ord;
63}
64
65/// An open tipping "motion". Retains all details of a tip including information on the finder
66/// and the members who have voted.
67#[storage_alias(dynamic)]
68type Tips<T: UnlockConfig<I>, I: 'static> = StorageMap<
69	<T as UnlockConfig<I>>::PalletName,
70	Twox64Concat,
71	<T as UnlockConfig<I>>::Hash,
72	crate::OpenTip<
73		<T as UnlockConfig<I>>::AccountId,
74		BalanceOf<T, I>,
75		<T as UnlockConfig<I>>::BlockNumber,
76		<T as UnlockConfig<I>>::Hash,
77	>,
78	OptionQuery,
79>;
80
81/// A migration that unreserves all tip deposits.
82///
83/// Useful to prevent funds from being locked up when the pallet is deprecated.
84///
85/// The pallet should be made inoperable before or immediately after this migration is run.
86///
87/// (See also the `RemovePallet` migration in `frame/support/src/migrations.rs`)
88pub struct UnreserveDeposits<T: UnlockConfig<I>, I: 'static>(core::marker::PhantomData<(T, I)>);
89
90impl<T: UnlockConfig<I>, I: 'static> UnreserveDeposits<T, I> {
91	/// Calculates and returns the total amount reserved by each account by this pallet from open
92	/// tips.
93	///
94	/// # Returns
95	///
96	/// * `BTreeMap<T::AccountId, T::Balance>`: Map of account IDs to their respective total
97	///   reserved balance by this pallet
98	/// * `frame_support::weights::Weight`: The weight of this operation.
99	fn get_deposits() -> (BTreeMap<T::AccountId, BalanceOf<T, I>>, frame_support::weights::Weight) {
100		use sp_core::Get;
101
102		let mut tips_len = 0;
103		let account_deposits: BTreeMap<T::AccountId, BalanceOf<T, I>> = Tips::<T, I>::iter()
104			.map(|(_hash, open_tip)| open_tip)
105			.fold(BTreeMap::new(), |mut acc, tip| {
106				// Count the total number of tips
107				tips_len.saturating_inc();
108
109				// Add the balance to the account's existing deposit in the accumulator
110				acc.entry(tip.finder).or_insert(Zero::zero()).saturating_accrue(tip.deposit);
111				acc
112			});
113
114		(account_deposits, T::DbWeight::get().reads(tips_len))
115	}
116}
117
118impl<T: UnlockConfig<I>, I: 'static> OnRuntimeUpgrade for UnreserveDeposits<T, I>
119where
120	BalanceOf<T, I>: Sum,
121{
122	/// Gets the actual reserved amount for each account before the migration, performs integrity
123	/// checks and prints some summary information.
124	///
125	/// Steps:
126	/// 1. Gets the deposited balances for each account stored in this pallet.
127	/// 2. Collects actual pre-migration reserved balances for each account.
128	/// 3. Checks the integrity of the deposited balances.
129	/// 4. Prints summary statistics about the state to be migrated.
130	/// 5. Returns the pre-migration actual reserved balance for each account that will
131	/// be part of the migration.
132	///
133	/// Fails with a `TryRuntimeError` if somehow the amount reserved by this pallet is greater than
134	/// the actual total reserved amount for any accounts.
135	#[cfg(feature = "try-runtime")]
136	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
137		use codec::Encode;
138		use frame_support::ensure;
139
140		// Get the Tips pallet view of balances it has reserved
141		let (account_deposits, _) = Self::get_deposits();
142
143		// Get the actual amounts reserved for accounts with open tips
144		let account_reserved_before: BTreeMap<T::AccountId, BalanceOf<T, I>> = account_deposits
145			.keys()
146			.map(|account| (account.clone(), T::Currency::reserved_balance(&account)))
147			.collect();
148
149		// The deposit amount must be less than or equal to the reserved amount.
150		// If it is higher, there is either a bug with the pallet or a bug in the calculation of the
151		// deposit amount.
152		ensure!(
153			account_deposits.iter().all(|(account, deposit)| *deposit <=
154				*account_reserved_before.get(account).unwrap_or(&Zero::zero())),
155			"Deposit amount is greater than reserved amount"
156		);
157
158		// Print some summary stats
159		let total_deposits_to_unreserve =
160			account_deposits.clone().into_values().sum::<BalanceOf<T, I>>();
161		log::info!(target: LOG_TARGET, "Total accounts: {}", account_deposits.keys().count());
162		log::info!(target: LOG_TARGET, "Total amount to unreserve: {:?}", total_deposits_to_unreserve);
163
164		// Return the actual amount reserved before the upgrade to verify integrity of the upgrade
165		// in the post_upgrade hook.
166		Ok(account_reserved_before.encode())
167	}
168
169	/// Executes the migration, unreserving funds that are locked in Tip deposits.
170	fn on_runtime_upgrade() -> frame_support::weights::Weight {
171		use frame_support::traits::Get;
172
173		// Get staked and deposited balances as reported by this pallet.
174		let (account_deposits, initial_reads) = Self::get_deposits();
175
176		// Deposited funds need to be unreserved.
177		for (account, unreserve_amount) in account_deposits.iter() {
178			if unreserve_amount.is_zero() {
179				continue
180			}
181			T::Currency::unreserve(&account, *unreserve_amount);
182		}
183
184		T::DbWeight::get()
185			.reads_writes(account_deposits.len() as u64, account_deposits.len() as u64)
186			.saturating_add(initial_reads)
187	}
188
189	/// Verifies that the account reserved balances were reduced by the actual expected amounts.
190	#[cfg(feature = "try-runtime")]
191	fn post_upgrade(
192		account_reserved_before_bytes: alloc::vec::Vec<u8>,
193	) -> Result<(), sp_runtime::TryRuntimeError> {
194		use codec::Decode;
195
196		let account_reserved_before = BTreeMap::<T::AccountId, BalanceOf<T, I>>::decode(
197			&mut &account_reserved_before_bytes[..],
198		)
199		.map_err(|_| "Failed to decode account_reserved_before_bytes")?;
200
201		// Get deposited balances as reported by this pallet.
202		let (account_deposits, _) = Self::get_deposits();
203
204		// Check that the reserved balance is reduced by the expected deposited amount.
205		for (account, actual_reserved_before) in account_reserved_before {
206			let actual_reserved_after = T::Currency::reserved_balance(&account);
207			let expected_amount_deducted = *account_deposits
208				.get(&account)
209				.expect("account deposit must exist to be in account_reserved_before, qed");
210			let expected_reserved_after =
211				actual_reserved_before.saturating_sub(expected_amount_deducted);
212
213			if actual_reserved_after != expected_reserved_after {
214				log::error!(
215					target: LOG_TARGET,
216					"Reserved balance for {:?} is incorrect. actual before: {:?}, actual after, {:?}, expected deducted: {:?}",
217					account,
218					actual_reserved_before,
219					actual_reserved_after,
220					expected_amount_deducted
221				);
222				return Err("Reserved balance is incorrect".into())
223			}
224		}
225
226		Ok(())
227	}
228}
229
230#[cfg(all(feature = "try-runtime", test))]
231mod test {
232	use super::*;
233	use crate::{
234		migrations::unreserve_deposits::UnreserveDeposits,
235		tests::{new_test_ext, Balances, RuntimeOrigin, Test, Tips},
236	};
237	use frame_support::{assert_ok, parameter_types, traits::TypedGet};
238	use frame_system::pallet_prelude::BlockNumberFor;
239	use sp_core::ConstU64;
240
241	parameter_types! {
242		const PalletName: &'static str = "Tips";
243	}
244
245	struct UnlockConfigImpl;
246	impl super::UnlockConfig<()> for UnlockConfigImpl {
247		type Currency = Balances;
248		type TipReportDepositBase = ConstU64<1>;
249		type DataDepositPerByte = ConstU64<1>;
250		type Hash = sp_core::H256;
251		type AccountId = u128;
252		type BlockNumber = BlockNumberFor<Test>;
253		type DbWeight = ();
254		type PalletName = PalletName;
255	}
256
257	#[test]
258	fn unreserve_all_funds_works() {
259		let tipper_0 = 0;
260		let tipper_1 = 1;
261		let tipper_0_initial_reserved = 0;
262		let tipper_1_initial_reserved = 5;
263		let recipient = 100;
264		let tip_0_reason = b"what_is_really_not_awesome".to_vec();
265		let tip_1_reason = b"pineapple_on_pizza".to_vec();
266		new_test_ext().execute_with(|| {
267			// Set up
268			assert_ok!(<Test as pallet_treasury::Config>::Currency::reserve(
269				&tipper_0,
270				tipper_0_initial_reserved
271			));
272			assert_ok!(<Test as pallet_treasury::Config>::Currency::reserve(
273				&tipper_1,
274				tipper_1_initial_reserved
275			));
276
277			// Make some tips
278			assert_ok!(Tips::report_awesome(
279				RuntimeOrigin::signed(tipper_0),
280				tip_0_reason.clone(),
281				recipient
282			));
283			assert_ok!(Tips::report_awesome(
284				RuntimeOrigin::signed(tipper_1),
285				tip_1_reason.clone(),
286				recipient
287			));
288
289			// Verify the expected amount is reserved
290			assert_eq!(
291				<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_0),
292				tipper_0_initial_reserved +
293					<Test as crate::Config>::TipReportDepositBase::get() +
294					<Test as crate::Config>::DataDepositPerByte::get() *
295						tip_0_reason.len() as u64
296			);
297			assert_eq!(
298				<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_1),
299				tipper_1_initial_reserved +
300					<Test as crate::Config>::TipReportDepositBase::get() +
301					<Test as crate::Config>::DataDepositPerByte::get() *
302						tip_1_reason.len() as u64
303			);
304
305			// Execute the migration
306			let bytes = match UnreserveDeposits::<UnlockConfigImpl, ()>::pre_upgrade() {
307				Ok(bytes) => bytes,
308				Err(e) => panic!("pre_upgrade failed: {:?}", e),
309			};
310			UnreserveDeposits::<UnlockConfigImpl, ()>::on_runtime_upgrade();
311			assert_ok!(UnreserveDeposits::<UnlockConfigImpl, ()>::post_upgrade(bytes));
312
313			// Check the deposits were were unreserved
314			assert_eq!(
315				<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_0),
316				tipper_0_initial_reserved
317			);
318			assert_eq!(
319				<Test as pallet_treasury::Config>::Currency::reserved_balance(&tipper_1),
320				tipper_1_initial_reserved
321			);
322		});
323	}
324}