referrerpolicy=no-referrer-when-downgrade

pallet_society/
migrations.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//! # Migrations for Society Pallet
19
20use super::*;
21use alloc::{vec, vec::Vec};
22use codec::{Decode, Encode};
23use core::cmp::Ordering;
24use frame_support::traits::{
25	Defensive, DefensiveOption, ExistenceRequirement::KeepAlive, Instance,
26	UncheckedOnRuntimeUpgrade,
27};
28
29#[cfg(feature = "try-runtime")]
30use sp_runtime::TryRuntimeError;
31
32/// The log target.
33const TARGET: &'static str = "runtime::society::migration";
34
35/// This migration moves all the state to v2 of Society.
36pub struct VersionUncheckedMigrateToV2<T: Config<I>, I: 'static, PastPayouts>(
37	core::marker::PhantomData<(T, I, PastPayouts)>,
38);
39
40impl<
41		T: Config<I>,
42		I: Instance + 'static,
43		PastPayouts: Get<Vec<(<T as frame_system::Config>::AccountId, BalanceOf<T, I>)>>,
44	> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateToV2<T, I, PastPayouts>
45{
46	#[cfg(feature = "try-runtime")]
47	fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
48		let in_code = Pallet::<T, I>::in_code_storage_version();
49		let on_chain = Pallet::<T, I>::on_chain_storage_version();
50		ensure!(on_chain == 0 && in_code == 2, "pallet_society: invalid version");
51
52		Ok((v0::Candidates::<T, I>::get(), v0::Members::<T, I>::get()).encode())
53	}
54
55	fn on_runtime_upgrade() -> Weight {
56		let onchain = Pallet::<T, I>::on_chain_storage_version();
57		if onchain < 2 {
58			log::info!(
59				target: TARGET,
60				"Running migration against onchain version {:?}",
61				onchain
62			);
63			from_original::<T, I>(&mut PastPayouts::get()).defensive_unwrap_or(Weight::MAX)
64		} else {
65			log::warn!("Unexpected onchain version: {:?} (expected 0)", onchain);
66			T::DbWeight::get().reads(1)
67		}
68	}
69
70	#[cfg(feature = "try-runtime")]
71	fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
72		let old: (
73			Vec<Bid<<T as frame_system::Config>::AccountId, BalanceOf<T, I>>>,
74			Vec<<T as frame_system::Config>::AccountId>,
75		) = Decode::decode(&mut &data[..]).expect("Bad data");
76		let mut old_candidates =
77			old.0.into_iter().map(|x| (x.who, x.kind, x.value)).collect::<Vec<_>>();
78		let mut old_members = old.1;
79		let mut candidates =
80			Candidates::<T, I>::iter().map(|(k, v)| (k, v.kind, v.bid)).collect::<Vec<_>>();
81		let mut members = Members::<T, I>::iter_keys().collect::<Vec<_>>();
82
83		old_candidates.sort_by_key(|x| x.0.clone());
84		candidates.sort_by_key(|x| x.0.clone());
85		assert_eq!(candidates, old_candidates);
86
87		members.sort();
88		old_members.sort();
89		assert_eq!(members, old_members);
90
91		ensure!(
92			Pallet::<T, I>::on_chain_storage_version() == 2,
93			"The onchain version must be updated after the migration."
94		);
95
96		assert_internal_consistency::<T, I>();
97		Ok(())
98	}
99}
100
101/// [`VersionUncheckedMigrateToV2`] wrapped in a [`frame_support::migrations::VersionedMigration`],
102/// ensuring the migration is only performed when on-chain version is 0.
103pub type MigrateToV2<T, I, PastPayouts> = frame_support::migrations::VersionedMigration<
104	0,
105	2,
106	VersionUncheckedMigrateToV2<T, I, PastPayouts>,
107	crate::pallet::Pallet<T, I>,
108	<T as frame_system::Config>::DbWeight,
109>;
110
111/// Reconcile the balance of the payouts account with the payouts recorded in storage.
112///
113/// The balance of the payouts account must equal the total of all pending payouts recorded in
114/// `Payouts`. Deployments may have drifted from this invariant — e.g. through code which discarded
115/// payout records without moving the balance backing them, or through
116/// [`VersionUncheckedMigrateToV2`], which carries payout records over without funding the account.
117/// This migration transfers the difference between the payouts account and the society account in
118/// whichever direction restores the invariant. It is unversioned, idempotent and safe to keep in a
119/// runtime's migration tuple across upgrades.
120pub struct ReconcilePayoutsAccount<T, I = ()>(core::marker::PhantomData<(T, I)>);
121
122impl<T: Config<I>, I: Instance + 'static> frame_support::traits::OnRuntimeUpgrade
123	for ReconcilePayoutsAccount<T, I>
124{
125	fn on_runtime_upgrade() -> Weight {
126		let entries = Payouts::<T, I>::iter_keys().count() as u64;
127		let pending = Pallet::<T, I>::pending_payouts_total();
128		let society_account = Pallet::<T, I>::account_id();
129		let payouts_account = Pallet::<T, I>::payouts();
130		let balance = T::Currency::free_balance(&payouts_account);
131
132		// Top-ups must never reap the society account; sweeps may reap the payouts account only
133		// when no pending payouts remain to be backed.
134		let res = match balance.cmp(&pending) {
135			Ordering::Equal => Ok(()),
136			Ordering::Less => T::Currency::transfer(
137				&society_account,
138				&payouts_account,
139				pending - balance,
140				KeepAlive,
141			),
142			Ordering::Greater if pending.is_zero() => {
143				T::Currency::transfer(&payouts_account, &society_account, balance, AllowDeath)
144			},
145			Ordering::Greater => T::Currency::transfer(
146				&payouts_account,
147				&society_account,
148				balance - pending,
149				KeepAlive,
150			),
151		};
152		if let Err(e) = res {
153			frame_support::defensive!("failed to reconcile the payouts account", e);
154		}
155
156		T::DbWeight::get().reads_writes(entries.saturating_add(2), 2)
157	}
158
159	#[cfg(feature = "try-runtime")]
160	fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
161		Pallet::<T, I>::do_try_state()
162	}
163}
164
165pub(crate) mod v0 {
166	use super::*;
167	use frame_support::storage_alias;
168
169	/// A vote by a member on a candidate application.
170	#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, Debug, TypeInfo)]
171	pub enum Vote {
172		/// The member has been chosen to be skeptic and has not yet taken any action.
173		Skeptic,
174		/// The member has rejected the candidate's application.
175		Reject,
176		/// The member approves of the candidate's application.
177		Approve,
178	}
179
180	#[storage_alias]
181	pub type Bids<T: Config<I>, I: 'static> = StorageValue<
182		Pallet<T, I>,
183		Vec<Bid<<T as frame_system::Config>::AccountId, BalanceOf<T, I>>>,
184		ValueQuery,
185	>;
186	#[storage_alias]
187	pub type Candidates<T: Config<I>, I: 'static> = StorageValue<
188		Pallet<T, I>,
189		Vec<Bid<<T as frame_system::Config>::AccountId, BalanceOf<T, I>>>,
190		ValueQuery,
191	>;
192	#[storage_alias]
193	pub type Votes<T: Config<I>, I: 'static> = StorageDoubleMap<
194		Pallet<T, I>,
195		Twox64Concat,
196		<T as frame_system::Config>::AccountId,
197		Twox64Concat,
198		<T as frame_system::Config>::AccountId,
199		Vote,
200	>;
201	#[storage_alias]
202	pub type SuspendedCandidates<T: Config<I>, I: 'static> = StorageMap<
203		Pallet<T, I>,
204		Twox64Concat,
205		<T as frame_system::Config>::AccountId,
206		(BalanceOf<T, I>, BidKind<<T as frame_system::Config>::AccountId, BalanceOf<T, I>>),
207	>;
208	#[storage_alias]
209	pub type Members<T: Config<I>, I: 'static> =
210		StorageValue<Pallet<T, I>, Vec<<T as frame_system::Config>::AccountId>, ValueQuery>;
211	#[storage_alias]
212	pub type Vouching<T: Config<I>, I: 'static> = StorageMap<
213		Pallet<T, I>,
214		Twox64Concat,
215		<T as frame_system::Config>::AccountId,
216		VouchingStatus,
217	>;
218	#[storage_alias]
219	pub type Strikes<T: Config<I>, I: 'static> = StorageMap<
220		Pallet<T, I>,
221		Twox64Concat,
222		<T as frame_system::Config>::AccountId,
223		StrikeCount,
224		ValueQuery,
225	>;
226	#[storage_alias]
227	pub type Payouts<T: Config<I>, I: 'static> = StorageMap<
228		Pallet<T, I>,
229		Twox64Concat,
230		<T as frame_system::Config>::AccountId,
231		Vec<(BlockNumberFor<T, I>, BalanceOf<T, I>)>,
232		ValueQuery,
233	>;
234	#[storage_alias]
235	pub type SuspendedMembers<T: Config<I>, I: 'static> = StorageMap<
236		Pallet<T, I>,
237		Twox64Concat,
238		<T as frame_system::Config>::AccountId,
239		bool,
240		ValueQuery,
241	>;
242	#[storage_alias]
243	pub type Defender<T: Config<I>, I: 'static> =
244		StorageValue<Pallet<T, I>, <T as frame_system::Config>::AccountId>;
245	#[storage_alias]
246	pub type DefenderVotes<T: Config<I>, I: 'static> =
247		StorageMap<Pallet<T, I>, Twox64Concat, <T as frame_system::Config>::AccountId, Vote>;
248}
249
250/// Will panic if there are any inconsistencies in the pallet's state or old keys remaining.
251pub fn assert_internal_consistency<T: Config<I>, I: Instance + 'static>() {
252	// Check all members are valid data.
253	let mut members = vec![];
254	for m in Members::<T, I>::iter_keys() {
255		let r = Members::<T, I>::get(&m).expect("Member data must be valid");
256		members.push((m, r));
257	}
258	assert_eq!(MemberCount::<T, I>::get(), members.len() as u32);
259	for (who, record) in members.iter() {
260		assert_eq!(MemberByIndex::<T, I>::get(record.index).as_ref(), Some(who));
261	}
262	if let Some(founder) = Founder::<T, I>::get() {
263		assert_eq!(Members::<T, I>::get(founder).expect("founder is member").index, 0);
264	}
265	if let Some(head) = Head::<T, I>::get() {
266		assert!(Members::<T, I>::contains_key(head));
267	}
268	// Check all votes are valid data.
269	for (k1, k2) in Votes::<T, I>::iter_keys() {
270		assert!(Votes::<T, I>::get(k1, k2).is_some());
271	}
272	// Check all defender votes are valid data.
273	for (k1, k2) in DefenderVotes::<T, I>::iter_keys() {
274		assert!(DefenderVotes::<T, I>::get(k1, k2).is_some());
275	}
276	// Check all candidates are valid data.
277	for k in Candidates::<T, I>::iter_keys() {
278		assert!(Candidates::<T, I>::get(k).is_some());
279	}
280	// Check all suspended members are valid data.
281	for m in SuspendedMembers::<T, I>::iter_keys() {
282		assert!(SuspendedMembers::<T, I>::get(m).is_some());
283	}
284	// Check all payouts are valid data.
285	for p in Payouts::<T, I>::iter_keys() {
286		let k = Payouts::<T, I>::hashed_key_for(&p);
287		let v = frame_support::storage::unhashed::get_raw(&k[..]).expect("value is in map");
288		assert!(PayoutRecordFor::<T, I>::decode(&mut &v[..]).is_ok());
289	}
290
291	// We don't use these - make sure they don't exist.
292	assert_eq!(v0::SuspendedCandidates::<T, I>::iter().count(), 0);
293	assert_eq!(v0::Strikes::<T, I>::iter().count(), 0);
294	assert_eq!(v0::Vouching::<T, I>::iter().count(), 0);
295	assert!(!v0::Defender::<T, I>::exists());
296	assert!(!v0::Members::<T, I>::exists());
297}
298
299pub fn from_original<T: Config<I>, I: Instance + 'static>(
300	past_payouts: &mut [(<T as frame_system::Config>::AccountId, BalanceOf<T, I>)],
301) -> Result<Weight, &'static str> {
302	// Migrate Bids from old::Bids (just a truncation).
303	Bids::<T, I>::put(BoundedVec::<_, T::MaxBids>::truncate_from(v0::Bids::<T, I>::take()));
304
305	// Initialise round counter.
306	RoundCount::<T, I>::put(0);
307
308	// Migrate Candidates from old::Candidates
309	for Bid { who: candidate, kind, value } in v0::Candidates::<T, I>::take().into_iter() {
310		let mut tally = Tally::default();
311		// Migrate Votes from old::Votes
312		// No need to drain, since we're overwriting values.
313		for (voter, vote) in v0::Votes::<T, I>::iter_prefix(&candidate) {
314			Votes::<T, I>::insert(
315				&candidate,
316				&voter,
317				Vote { approve: vote == v0::Vote::Approve, weight: 1 },
318			);
319			match vote {
320				v0::Vote::Approve => tally.approvals.saturating_inc(),
321				v0::Vote::Reject => tally.rejections.saturating_inc(),
322				v0::Vote::Skeptic => Skeptic::<T, I>::put(&voter),
323			}
324		}
325		Candidates::<T, I>::insert(
326			&candidate,
327			Candidacy { round: 0, kind, tally, skeptic_struck: false, bid: value },
328		);
329	}
330
331	// Migrate Members from old::Members old::Strikes old::Vouching
332	let mut member_count = 0;
333	for member in v0::Members::<T, I>::take() {
334		let strikes = v0::Strikes::<T, I>::take(&member);
335		let vouching = v0::Vouching::<T, I>::take(&member);
336		let record = MemberRecord { index: member_count, rank: 0, strikes, vouching };
337		Members::<T, I>::insert(&member, record);
338		MemberByIndex::<T, I>::insert(member_count, &member);
339
340		// The founder must be the first member in Society V2. If we find the founder not in index
341		// zero, we swap it with the first member.
342		if member == Founder::<T, I>::get().defensive_ok_or("founder must always be set")? &&
343			member_count > 0
344		{
345			let member_to_swap = MemberByIndex::<T, I>::get(0)
346				.defensive_ok_or("member_count > 0, we must have at least 1 member")?;
347			// Swap the founder with the first member in MemberByIndex.
348			MemberByIndex::<T, I>::swap(0, member_count);
349			// Update the indices of the swapped member MemberRecords.
350			Members::<T, I>::mutate(&member, |m| {
351				if let Some(member) = m {
352					member.index = 0;
353				} else {
354					frame_support::defensive!(
355						"Member somehow disappeared from storage after it was inserted"
356					);
357				}
358			});
359			Members::<T, I>::mutate(&member_to_swap, |m| {
360				if let Some(member) = m {
361					member.index = member_count;
362				} else {
363					frame_support::defensive!(
364						"Member somehow disappeared from storage after it was queried"
365					);
366				}
367			});
368		}
369		member_count.saturating_inc();
370	}
371	MemberCount::<T, I>::put(member_count);
372
373	// Migrate Payouts from: old::Payouts and raw info (needed since we can't query old chain
374	// state).
375	past_payouts.sort();
376	for (who, mut payouts) in v0::Payouts::<T, I>::iter() {
377		payouts.truncate(T::MaxPayouts::get() as usize);
378		// ^^ Safe since we already truncated.
379		let paid = past_payouts
380			.binary_search_by_key(&&who, |x| &x.0)
381			.ok()
382			.map(|p| past_payouts[p].1)
383			.unwrap_or(Zero::zero());
384		match BoundedVec::try_from(payouts) {
385			Ok(payouts) => Payouts::<T, I>::insert(who, PayoutRecord { paid, payouts }),
386			Err(_) => debug_assert!(false, "Truncation of Payouts ineffective??"),
387		}
388	}
389
390	// Migrate SuspendedMembers from old::SuspendedMembers old::Strikes old::Vouching.
391	for who in v0::SuspendedMembers::<T, I>::iter_keys() {
392		let strikes = v0::Strikes::<T, I>::take(&who);
393		let vouching = v0::Vouching::<T, I>::take(&who);
394		let record = MemberRecord { index: 0, rank: 0, strikes, vouching };
395		SuspendedMembers::<T, I>::insert(&who, record);
396	}
397
398	// Any suspended candidates remaining are rejected.
399	let _ = v0::SuspendedCandidates::<T, I>::clear(u32::MAX, None);
400
401	// We give the current defender the benefit of the doubt.
402	v0::Defender::<T, I>::kill();
403	let _ = v0::DefenderVotes::<T, I>::clear(u32::MAX, None);
404
405	Ok(T::BlockWeights::get().max_block)
406}
407
408pub fn from_raw_past_payouts<T: Config<I>, I: Instance + 'static>(
409	past_payouts_raw: impl Iterator<Item = ([u8; 32], u128)>,
410) -> Vec<(<T as frame_system::Config>::AccountId, BalanceOf<T, I>)> {
411	past_payouts_raw
412		.filter_map(|(x, y)| Some((Decode::decode(&mut &x[..]).ok()?, y.try_into().ok()?)))
413		.collect()
414}