referrerpolicy=no-referrer-when-downgrade

pallet_elections_phragmen/migrations/
unlock_and_unreserve_all_funds.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, vec::Vec};
22use core::iter::Sum;
23use frame_support::{
24	pallet_prelude::ValueQuery,
25	storage_alias,
26	traits::{Currency, LockIdentifier, LockableCurrency, OnRuntimeUpgrade, ReservableCurrency},
27	weights::RuntimeDbWeight,
28	Parameter, Twox64Concat,
29};
30use sp_core::Get;
31use sp_runtime::traits::Zero;
32
33const LOG_TARGET: &str = "elections_phragmen::migrations::unlock_and_unreserve_all_funds";
34
35type BalanceOf<T> =
36	<<T as UnlockConfig>::Currency as Currency<<T as UnlockConfig>::AccountId>>::Balance;
37
38/// The configuration for [`UnlockAndUnreserveAllFunds`].
39pub trait UnlockConfig: 'static {
40	/// The account ID used in the runtime.
41	type AccountId: Parameter + Ord;
42	/// The currency type used in the runtime.
43	///
44	/// Should match the currency type previously used for the pallet, if applicable.
45	type Currency: LockableCurrency<Self::AccountId> + ReservableCurrency<Self::AccountId>;
46	/// The name of the pallet as previously configured in
47	/// [`construct_runtime!`](frame_support::construct_runtime).
48	type PalletName: Get<&'static str>;
49	/// The maximum number of votes per voter as configured previously in the previous runtime.
50	type MaxVotesPerVoter: Get<u32>;
51	/// Identifier for the elections-phragmen pallet's lock, as previously configured in the
52	/// runtime.
53	type PalletId: Get<LockIdentifier>;
54	/// The DB weight as configured in the runtime to calculate the correct weight.
55	type DbWeight: Get<RuntimeDbWeight>;
56}
57
58#[storage_alias(dynamic)]
59type Members<T: UnlockConfig> = StorageValue<
60	<T as UnlockConfig>::PalletName,
61	Vec<crate::SeatHolder<<T as UnlockConfig>::AccountId, BalanceOf<T>>>,
62	ValueQuery,
63>;
64
65#[storage_alias(dynamic)]
66type RunnersUp<T: UnlockConfig> = StorageValue<
67	<T as UnlockConfig>::PalletName,
68	Vec<crate::SeatHolder<<T as UnlockConfig>::AccountId, BalanceOf<T>>>,
69	ValueQuery,
70>;
71
72#[storage_alias(dynamic)]
73type Candidates<T: UnlockConfig> = StorageValue<
74	<T as UnlockConfig>::PalletName,
75	Vec<(<T as UnlockConfig>::AccountId, BalanceOf<T>)>,
76	ValueQuery,
77>;
78
79#[storage_alias(dynamic)]
80type Voting<T: UnlockConfig> = StorageMap<
81	<T as UnlockConfig>::PalletName,
82	Twox64Concat,
83	<T as UnlockConfig>::AccountId,
84	crate::Voter<<T as UnlockConfig>::AccountId, BalanceOf<T>>,
85	ValueQuery,
86>;
87
88/// A migration that unreserves all deposit and unlocks all stake held in the context of this
89/// pallet.
90///
91/// Useful to prevent funds from being locked up when the pallet is being deprecated.
92///
93/// The pallet should be made inoperable before this migration is run.
94///
95/// (See also [`RemovePallet`][frame_support::migrations::RemovePallet])
96pub struct UnlockAndUnreserveAllFunds<T: UnlockConfig>(core::marker::PhantomData<T>);
97
98impl<T: UnlockConfig> UnlockAndUnreserveAllFunds<T> {
99	/// Calculates and returns the total amounts deposited and staked by each account in the context
100	/// of this pallet.
101	///
102	/// The deposited and staked amounts are returned in two separate `BTreeMap` collections.
103	///
104	/// The first `BTreeMap`, `account_deposited_sums`, contains each account's total amount
105	/// deposited. This includes deposits made by Members, RunnerUps, Candidates, and Voters.
106	///
107	/// The second `BTreeMap`, `account_staked_sums`, contains each account's total amount staked.
108	/// This includes stakes made by Voters.
109	///
110	/// # Returns
111	///
112	/// This function returns a tuple of two `BTreeMap` collections and the weight of the reads:
113	///
114	/// * `BTreeMap<T::AccountId, BalanceOf<T>>`: Map of account IDs to their respective total
115	///   deposit sums.
116	/// * `BTreeMap<T::AccountId, BalanceOf<T>>`: Map of account IDs to their respective total
117	///   staked sums.
118	/// * `frame_support::weights::Weight`: The weight of reading the storage.
119	fn get_account_deposited_and_staked_sums() -> (
120		BTreeMap<T::AccountId, BalanceOf<T>>,
121		BTreeMap<T::AccountId, BalanceOf<T>>,
122		frame_support::weights::Weight,
123	) {
124		use sp_runtime::Saturating;
125
126		let members = Members::<T>::get();
127		let runner_ups = RunnersUp::<T>::get();
128		let candidates = Candidates::<T>::get();
129
130		// Get the total amount deposited (Members, RunnerUps, Candidates and Voters all can have
131		// deposits).
132		let account_deposited_sums: BTreeMap<T::AccountId, BalanceOf<T>> = members
133			// Massage all data structures into (account_id, deposit) tuples.
134			.iter()
135			.chain(runner_ups.iter())
136			.map(|member| (member.who.clone(), member.deposit))
137			.chain(candidates.iter().map(|(candidate, amount)| (candidate.clone(), *amount)))
138			.chain(
139				Voting::<T>::iter().map(|(account_id, voter)| (account_id.clone(), voter.deposit)),
140			)
141			// Finally, aggregate the tuples into a Map.
142			.fold(BTreeMap::new(), |mut acc, (id, deposit)| {
143				acc.entry(id.clone()).or_insert(Zero::zero()).saturating_accrue(deposit);
144				acc
145			});
146
147		// Get the total amount staked (only Voters stake) and count the number of voters.
148		let mut voters_len = 0;
149		let account_staked_sums: BTreeMap<T::AccountId, BalanceOf<T>> = Voting::<T>::iter()
150			.map(|(account_id, voter)| (account_id.clone(), voter.stake))
151			.fold(BTreeMap::new(), |mut acc, (id, stake)| {
152				voters_len.saturating_accrue(1);
153				acc.entry(id.clone()).or_insert(Zero::zero()).saturating_accrue(stake);
154				acc
155			});
156
157		(
158			account_deposited_sums,
159			account_staked_sums,
160			T::DbWeight::get().reads(
161				members
162					.len()
163					.saturating_add(runner_ups.len())
164					.saturating_add(candidates.len())
165					.saturating_add(voters_len.saturating_mul(T::MaxVotesPerVoter::get() as usize))
166					as u64,
167			),
168		)
169	}
170}
171
172impl<T: UnlockConfig> OnRuntimeUpgrade for UnlockAndUnreserveAllFunds<T>
173where
174	BalanceOf<T>: Sum,
175{
176	/// Collects pre-migration data useful for validating the migration was successful, and also
177	/// checks the integrity of deposited and reserved balances.
178	///
179	/// Steps:
180	/// 1. Gets the deposited and staked balances for each account stored in this pallet.
181	/// 2. Collects actual pre-migration locked and reserved balances for each account.
182	/// 3. Checks the integrity of the deposited and reserved balances.
183	/// 4. Prints summary statistics about the state to be migrated.
184	/// 5. Encodes and returns pre-migration data to be used in post_upgrade.
185	///
186	/// Fails with a `TryRuntimeError` if there's a discrepancy between the amount
187	/// reported as staked by the pallet and the amount actually locked in `Balances`.
188	#[cfg(feature = "try-runtime")]
189	fn pre_upgrade() -> Result<Vec<u8>, sp_runtime::TryRuntimeError> {
190		use alloc::collections::btree_set::BTreeSet;
191		use codec::Encode;
192
193		// Get staked and deposited balances as reported by this pallet.
194		let (account_deposited_sums, account_staked_sums, _) =
195			Self::get_account_deposited_and_staked_sums();
196
197		let all_accounts: BTreeSet<T::AccountId> = account_staked_sums
198			.keys()
199			.chain(account_deposited_sums.keys())
200			.cloned()
201			.collect();
202
203		let account_reserved_before: BTreeMap<T::AccountId, BalanceOf<T>> = all_accounts
204			.iter()
205			.map(|account| (account.clone(), T::Currency::reserved_balance(&account)))
206			.collect();
207
208		// Total deposited for each account *should* be less than or equal to the total reserved,
209		// however this does not hold for all cases due to bugs in the reserve logic of this pallet.
210		let bugged_deposits = all_accounts
211			.iter()
212			.filter(|account| {
213				account_deposited_sums.get(&account).unwrap_or(&Zero::zero()) >
214					account_reserved_before.get(&account).unwrap_or(&Zero::zero())
215			})
216			.count();
217
218		// Print some summary stats.
219		let total_stake_to_unlock = account_staked_sums.clone().into_values().sum::<BalanceOf<T>>();
220		let total_deposits_to_unreserve =
221			account_deposited_sums.clone().into_values().sum::<BalanceOf<T>>();
222		log::info!(target: LOG_TARGET, "Total accounts: {:?}", all_accounts.len());
223		log::info!(target: LOG_TARGET, "Total stake to unlock: {:?}", total_stake_to_unlock);
224		log::info!(
225			target: LOG_TARGET,
226			"Total deposit to unreserve: {:?}",
227			total_deposits_to_unreserve
228		);
229		if bugged_deposits > 0 {
230			log::warn!(
231				target: LOG_TARGET,
232				"Bugged deposits: {}/{}",
233				bugged_deposits,
234				all_accounts.len()
235			);
236		}
237
238		Ok(account_reserved_before.encode())
239	}
240
241	/// Executes the migration.
242	///
243	/// Steps:
244	/// 1. Retrieves the deposit and stake amounts from the pallet.
245	/// 2. Unreserves the deposited funds for each account.
246	/// 3. Unlocks the staked funds for each account.
247	fn on_runtime_upgrade() -> frame_support::weights::Weight {
248		// Get staked and deposited balances as reported by this pallet.
249		let (account_deposited_sums, account_staked_sums, initial_reads) =
250			Self::get_account_deposited_and_staked_sums();
251
252		// Deposited funds need to be unreserved.
253		for (account, unreserve_amount) in account_deposited_sums.iter() {
254			if unreserve_amount.is_zero() {
255				log::warn!(target: LOG_TARGET, "Unexpected zero amount to unreserve");
256				continue
257			}
258			T::Currency::unreserve(&account, *unreserve_amount);
259		}
260
261		// Staked funds need to be unlocked.
262		for (account, amount) in account_staked_sums.iter() {
263			if amount.is_zero() {
264				log::warn!(target: LOG_TARGET, "Unexpected zero amount to unlock");
265				continue
266			}
267			T::Currency::remove_lock(T::PalletId::get(), account);
268		}
269
270		T::DbWeight::get()
271			.reads_writes(
272				(account_deposited_sums.len().saturating_add(account_staked_sums.len())) as u64,
273				(account_deposited_sums.len().saturating_add(account_staked_sums.len())) as u64,
274			)
275			.saturating_add(initial_reads)
276	}
277
278	/// Performs post-upgrade sanity checks:
279	///
280	/// 1. All expected locks were removed after the migration.
281	/// 2. The reserved balance for each account has been reduced by the expected amount.
282	#[cfg(feature = "try-runtime")]
283	fn post_upgrade(
284		account_reserved_before_bytes: Vec<u8>,
285	) -> Result<(), sp_runtime::TryRuntimeError> {
286		use codec::Decode;
287		use sp_runtime::Saturating;
288
289		let account_reserved_before =
290			BTreeMap::<T::AccountId, BalanceOf<T>>::decode(&mut &account_reserved_before_bytes[..])
291				.map_err(|_| "Failed to decode account_reserved_before_bytes")?;
292
293		// Get deposited balances as reported by this pallet.
294		let (account_deposited_sums, _, _) = Self::get_account_deposited_and_staked_sums();
295
296		// Check that the reserved balance is reduced by the expected deposited amount.
297		for (account, actual_reserved_before) in account_reserved_before {
298			let actual_reserved_after = T::Currency::reserved_balance(&account);
299			let expected_amount_deducted = *account_deposited_sums
300				.get(&account)
301				.unwrap_or(&Zero::zero())
302				// .min here to handle bugged deposits where actual_reserved_before is less than the
303				// amount the pallet reports is reserved
304				.min(&actual_reserved_before);
305			let expected_reserved_after =
306				actual_reserved_before.saturating_sub(expected_amount_deducted);
307			assert!(
308				actual_reserved_after == expected_reserved_after,
309				"Reserved balance for {:?} is incorrect. actual before: {:?}, actual after, {:?}, expected deducted: {:?}",
310				account,
311				actual_reserved_before,
312				actual_reserved_after,
313				expected_amount_deducted,
314			);
315		}
316
317		Ok(())
318	}
319}
320
321#[cfg(all(feature = "try-runtime", test))]
322mod test {
323	use super::*;
324	use crate::{
325		tests::{Balances, ElectionsPhragmenPalletId, ExtBuilder, PhragmenMaxVoters, Test},
326		Candidates, Members, RunnersUp, SeatHolder, Voter, Voting,
327	};
328	use frame_support::{
329		assert_ok, parameter_types,
330		traits::{Currency, OnRuntimeUpgrade, ReservableCurrency, WithdrawReasons},
331	};
332
333	parameter_types! {
334		const PalletName: &'static str = "Elections";
335	}
336
337	struct UnlockConfigImpl;
338	impl super::UnlockConfig for UnlockConfigImpl {
339		type Currency = Balances;
340		type AccountId = u64;
341		type DbWeight = ();
342		type PalletName = PalletName;
343		type MaxVotesPerVoter = PhragmenMaxVoters;
344		type PalletId = ElectionsPhragmenPalletId;
345	}
346
347	#[test]
348	fn unreserve_works_for_candidate() {
349		let candidate = 10;
350		let deposit = 100;
351		let initial_reserved = 15;
352		let initial_balance = 100_000;
353		ExtBuilder::default().build_and_execute(|| {
354			// Set up initial state.
355			<Test as crate::Config>::Currency::make_free_balance_be(&candidate, initial_balance);
356			assert_ok!(<Test as crate::Config>::Currency::reserve(&candidate, initial_reserved));
357			Candidates::<Test>::set(vec![(candidate, deposit)]);
358			assert_ok!(<Test as crate::Config>::Currency::reserve(&candidate, deposit));
359
360			// Sanity check: ensure initial Balance state was set up correctly.
361			assert_eq!(
362				<Test as crate::Config>::Currency::reserved_balance(&candidate),
363				deposit + initial_reserved
364			);
365
366			// Run the migration.
367			let bytes = UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::pre_upgrade()
368				.unwrap_or_else(|e| panic!("pre_upgrade failed: {:?}", e));
369			UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::on_runtime_upgrade();
370			assert_ok!(UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::post_upgrade(bytes));
371
372			// Assert the candidate reserved balance was reduced by the expected amount.
373			assert_eq!(
374				<Test as crate::Config>::Currency::reserved_balance(&candidate),
375				initial_reserved
376			);
377		});
378	}
379
380	#[test]
381	fn unreserve_works_for_runner_up() {
382		let runner_up = 10;
383		let deposit = 100;
384		let initial_reserved = 15;
385		let initial_balance = 100_000;
386		ExtBuilder::default().build_and_execute(|| {
387			// Set up initial state.
388			<Test as crate::Config>::Currency::make_free_balance_be(&runner_up, initial_balance);
389			assert_ok!(<Test as crate::Config>::Currency::reserve(&runner_up, initial_reserved));
390			RunnersUp::<Test>::set(vec![SeatHolder { who: runner_up, deposit, stake: 10 }]);
391			assert_ok!(<Test as crate::Config>::Currency::reserve(&runner_up, deposit));
392
393			// Sanity check: ensure initial Balance state was set up correctly.
394			assert_eq!(
395				<Test as crate::Config>::Currency::reserved_balance(&runner_up),
396				deposit + initial_reserved
397			);
398
399			// Run the migration.
400			let bytes = UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::pre_upgrade()
401				.unwrap_or_else(|e| panic!("pre_upgrade failed: {:?}", e));
402			UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::on_runtime_upgrade();
403			assert_ok!(UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::post_upgrade(bytes));
404
405			// Assert the reserved balance was reduced by the expected amount.
406			assert_eq!(
407				<Test as crate::Config>::Currency::reserved_balance(&runner_up),
408				initial_reserved
409			);
410		});
411	}
412
413	#[test]
414	fn unreserve_works_for_member() {
415		let member = 10;
416		let deposit = 100;
417		let initial_reserved = 15;
418		let initial_balance = 100_000;
419		ExtBuilder::default().build_and_execute(|| {
420			// Set up initial state.
421			<Test as crate::Config>::Currency::make_free_balance_be(&member, initial_balance);
422			assert_ok!(<Test as crate::Config>::Currency::reserve(&member, initial_reserved));
423			Members::<Test>::set(vec![SeatHolder { who: member, deposit, stake: 10 }]);
424			assert_ok!(<Test as crate::Config>::Currency::reserve(&member, deposit));
425
426			// Sanity check: ensure initial Balance state was set up correctly.
427			assert_eq!(
428				<Test as crate::Config>::Currency::reserved_balance(&member),
429				deposit + initial_reserved
430			);
431
432			// Run the migration.
433			let bytes = UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::pre_upgrade()
434				.unwrap_or_else(|e| panic!("pre_upgrade failed: {:?}", e));
435			UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::on_runtime_upgrade();
436			assert_ok!(UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::post_upgrade(bytes));
437
438			// Assert the reserved balance was reduced by the expected amount.
439			assert_eq!(
440				<Test as crate::Config>::Currency::reserved_balance(&member),
441				initial_reserved
442			);
443		});
444	}
445
446	#[test]
447	fn unlock_and_unreserve_works_for_voter() {
448		let voter = 10;
449		let deposit = 100;
450		let initial_reserved = 15;
451		let initial_locks = vec![(b"somethin", 10)];
452		let stake = 25;
453		let initial_balance = 100_000;
454		ExtBuilder::default().build_and_execute(|| {
455			let pallet_id = <Test as crate::Config>::PalletId::get();
456
457			// Set up initial state.
458			<Test as crate::Config>::Currency::make_free_balance_be(&voter, initial_balance);
459			assert_ok!(<Test as crate::Config>::Currency::reserve(&voter, initial_reserved));
460			for lock in initial_locks.clone() {
461				<Test as crate::Config>::Currency::set_lock(
462					*lock.0,
463					&voter,
464					lock.1,
465					WithdrawReasons::all(),
466				);
467			}
468			Voting::<Test>::insert(voter, Voter { votes: vec![], deposit, stake });
469			assert_ok!(<Test as crate::Config>::Currency::reserve(&voter, deposit));
470			<Test as crate::Config>::Currency::set_lock(
471				<Test as crate::Config>::PalletId::get(),
472				&voter,
473				stake,
474				WithdrawReasons::all(),
475			);
476
477			// Sanity check: ensure initial Balance state was set up correctly.
478			assert_eq!(
479				<Test as crate::Config>::Currency::reserved_balance(&voter),
480				deposit + initial_reserved
481			);
482			let mut voter_all_locks = initial_locks.clone();
483			voter_all_locks.push((&pallet_id, stake));
484			assert_eq!(
485				<Test as crate::Config>::Currency::locks(&voter)
486					.iter()
487					.map(|lock| (&lock.id, lock.amount))
488					.collect::<Vec<_>>(),
489				voter_all_locks
490			);
491
492			// Run the migration.
493			let bytes = UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::pre_upgrade()
494				.unwrap_or_else(|e| panic!("pre_upgrade failed: {:?}", e));
495			UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::on_runtime_upgrade();
496			assert_ok!(UnlockAndUnreserveAllFunds::<UnlockConfigImpl>::post_upgrade(bytes));
497
498			// Assert the voter lock was removed and the reserved balance was reduced by the
499			// expected amount.
500			assert_eq!(
501				<Test as crate::Config>::Currency::reserved_balance(&voter),
502				initial_reserved
503			);
504			assert_eq!(
505				<Test as crate::Config>::Currency::locks(&voter)
506					.iter()
507					.map(|lock| (&lock.id, lock.amount))
508					.collect::<Vec<_>>(),
509				initial_locks
510			);
511		});
512	}
513}