referrerpolicy=no-referrer-when-downgrade

pallet_nomination_pools/
migration.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
18use super::*;
19use crate::log;
20use alloc::{collections::btree_map::BTreeMap, vec::Vec};
21use frame_support::traits::{OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade};
22
23#[cfg(feature = "try-runtime")]
24use sp_runtime::TryRuntimeError;
25
26/// Exports for versioned migration `type`s for this pallet.
27pub mod versioned {
28	use super::*;
29
30	/// v8: Adds commission claim permissions to `BondedPools`.
31	pub type V7ToV8<T> = frame_support::migrations::VersionedMigration<
32		7,
33		8,
34		v8::VersionUncheckedMigrateV7ToV8<T>,
35		crate::pallet::Pallet<T>,
36		<T as frame_system::Config>::DbWeight,
37	>;
38
39	/// Migration V6 to V7 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring
40	/// the migration is only performed when on-chain version is 6.
41	pub type V6ToV7<T> = frame_support::migrations::VersionedMigration<
42		6,
43		7,
44		v7::VersionUncheckedMigrateV6ToV7<T>,
45		crate::pallet::Pallet<T>,
46		<T as frame_system::Config>::DbWeight,
47	>;
48
49	/// Wrapper over `MigrateToV6` with convenience version checks.
50	pub type V5toV6<T> = frame_support::migrations::VersionedMigration<
51		5,
52		6,
53		v6::MigrateToV6<T>,
54		crate::pallet::Pallet<T>,
55		<T as frame_system::Config>::DbWeight,
56	>;
57}
58
59pub mod unversioned {
60	use super::*;
61
62	/// Checks and updates `TotalValueLocked` if out of sync.
63	pub struct TotalValueLockedSync<T>(core::marker::PhantomData<T>);
64	impl<T: Config> OnRuntimeUpgrade for TotalValueLockedSync<T> {
65		#[cfg(feature = "try-runtime")]
66		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
67			Ok(Vec::new())
68		}
69
70		fn on_runtime_upgrade() -> Weight {
71			let migrated = BondedPools::<T>::count();
72
73			// recalculate the `TotalValueLocked` to compare with the current on-chain TVL which may
74			// be out of sync.
75			let tvl: BalanceOf<T> = helpers::calculate_tvl_by_total_stake::<T>();
76			let onchain_tvl = TotalValueLocked::<T>::get();
77
78			let writes = if tvl != onchain_tvl {
79				TotalValueLocked::<T>::set(tvl);
80
81				log!(
82					info,
83					"on-chain TVL was out of sync, update. Old: {:?}, new: {:?}",
84					onchain_tvl,
85					tvl
86				);
87
88				// writes: onchain version + set total value locked.
89				2
90			} else {
91				log!(info, "on-chain TVL was OK: {:?}", tvl);
92
93				// writes: onchain version write.
94				1
95			};
96
97			// reads: migrated * (BondedPools +  Staking::total_stake) + count + onchain
98			// version
99			//
100			// writes: current version + (maybe) TVL
101			T::DbWeight::get()
102				.reads_writes(migrated.saturating_mul(2).saturating_add(2).into(), writes)
103		}
104
105		#[cfg(feature = "try-runtime")]
106		fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
107			Ok(())
108		}
109	}
110
111	/// Migrate existing pools from [`adapter::StakeStrategyType::Transfer`] to
112	/// [`adapter::StakeStrategyType::Delegate`].
113	///
114	/// Note: This only migrates the pools, the members are not migrated. They can use the
115	/// permission-less [`Pallet::migrate_delegation()`] to migrate their funds.
116	///
117	/// This migration does not break any existing pool storage item, does not need to happen in any
118	/// sequence and hence can be applied unversioned on a production runtime.
119	///
120	/// Takes `MaxPools` as type parameter to limit the number of pools that should be migrated in a
121	/// single block. It should be set such that migration weight does not exceed the block weight
122	/// limit. If all pools can be safely migrated, it is good to keep this number a little higher
123	/// than the actual number of pools to handle any extra pools created while the migration is
124	/// proposed, and before it is executed.
125	///
126	/// If there are pools that fail to migrate or did not fit in the bounds, the remaining pools
127	/// can be migrated via the permission-less extrinsic [`Call::migrate_pool_to_delegate_stake`].
128	pub struct DelegationStakeMigration<T, MaxPools>(core::marker::PhantomData<(T, MaxPools)>);
129
130	impl<T: Config, MaxPools: Get<u32>> OnRuntimeUpgrade for DelegationStakeMigration<T, MaxPools> {
131		fn on_runtime_upgrade() -> Weight {
132			let mut count: u32 = 0;
133
134			BondedPools::<T>::iter_keys().take(MaxPools::get() as usize).for_each(|id| {
135				let pool_acc = Pallet::<T>::generate_bonded_account(id);
136
137				// only migrate if the pool is in Transfer Strategy.
138				if T::StakeAdapter::pool_strategy(Pool::from(pool_acc)) ==
139					adapter::StakeStrategyType::Transfer
140				{
141					let _ = Pallet::<T>::migrate_to_delegate_stake(id).map_err(|err| {
142						log!(
143							warn,
144							"failed to migrate pool {:?} to delegate stake strategy with err: {:?}",
145							id,
146							err
147						)
148					});
149					count.saturating_inc();
150				}
151			});
152
153			log!(info, "migrated {:?} pools to delegate stake strategy", count);
154
155			// reads: (bonded pool key + current pool strategy) * MaxPools (worst case)
156			T::DbWeight::get()
157				.reads_writes(2, 0)
158				.saturating_mul(MaxPools::get() as u64)
159				// migration weight: `pool_migrate` weight * count
160				.saturating_add(T::WeightInfo::pool_migrate().saturating_mul(count.into()))
161		}
162
163		#[cfg(feature = "try-runtime")]
164		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
165			// ensure stake adapter is correct.
166			ensure!(
167				T::StakeAdapter::strategy_type() == adapter::StakeStrategyType::Delegate,
168				"Current strategy is not `Delegate"
169			);
170
171			if BondedPools::<T>::count() > MaxPools::get() {
172				// we log a warning if the number of pools exceeds the bound.
173				log!(
174					warn,
175					"Number of pools {} exceeds the maximum bound {}. This would leave some pools unmigrated.", BondedPools::<T>::count(), MaxPools::get()
176				);
177			}
178
179			let mut pool_balances: Vec<BalanceOf<T>> = Vec::new();
180			BondedPools::<T>::iter_keys().take(MaxPools::get() as usize).for_each(|id| {
181				let pool_account = Pallet::<T>::generate_bonded_account(id);
182
183				// we ensure migration is idempotent.
184				let pool_balance = T::StakeAdapter::total_balance(Pool::from(pool_account.clone()))
185					// we check actual account balance if pool has not migrated yet.
186					.unwrap_or(T::Currency::total_balance(&pool_account));
187
188				pool_balances.push(pool_balance);
189			});
190
191			Ok(pool_balances.encode())
192		}
193
194		#[cfg(feature = "try-runtime")]
195		fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
196			let expected_pool_balances: Vec<BalanceOf<T>> = Decode::decode(&mut &data[..]).unwrap();
197
198			for (index, id) in
199				BondedPools::<T>::iter_keys().take(MaxPools::get() as usize).enumerate()
200			{
201				let pool_account = Pallet::<T>::generate_bonded_account(id);
202				if T::StakeAdapter::pool_strategy(Pool::from(pool_account.clone())) ==
203					adapter::StakeStrategyType::Transfer
204				{
205					log!(error, "Pool {} failed to migrate", id,);
206					return Err(TryRuntimeError::Other("Pool failed to migrate"));
207				}
208
209				let actual_balance =
210					T::StakeAdapter::total_balance(Pool::from(pool_account.clone()))
211						.expect("after migration, this should return a value");
212				let expected_balance = expected_pool_balances.get(index).unwrap();
213
214				if actual_balance != *expected_balance {
215					log!(
216						error,
217						"Pool {} balance mismatch. Expected: {:?}, Actual: {:?}",
218						id,
219						expected_balance,
220						actual_balance
221					);
222					return Err(TryRuntimeError::Other("Pool balance mismatch"));
223				}
224
225				// account balance should be zero.
226				let pool_account_balance = T::Currency::total_balance(&pool_account);
227				if pool_account_balance != Zero::zero() {
228					log!(
229						error,
230						"Pool account balance was expected to be zero. Pool: {}, Balance: {:?}",
231						id,
232						pool_account_balance
233					);
234					return Err(TryRuntimeError::Other("Pool account balance not migrated"));
235				}
236			}
237
238			Ok(())
239		}
240	}
241}
242
243pub mod v8 {
244	use super::{v7::V7BondedPoolInner, *};
245
246	impl<T: Config> V7BondedPoolInner<T> {
247		fn migrate_to_v8(self) -> BondedPoolInner<T> {
248			BondedPoolInner {
249				commission: Commission {
250					current: self.commission.current,
251					max: self.commission.max,
252					change_rate: self.commission.change_rate,
253					throttle_from: self.commission.throttle_from,
254					// `claim_permission` is a new field.
255					claim_permission: None,
256				},
257				member_counter: self.member_counter,
258				points: self.points,
259				roles: self.roles,
260				state: self.state,
261			}
262		}
263	}
264
265	pub struct VersionUncheckedMigrateV7ToV8<T>(core::marker::PhantomData<T>);
266	impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV7ToV8<T> {
267		#[cfg(feature = "try-runtime")]
268		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
269			Ok(Vec::new())
270		}
271
272		fn on_runtime_upgrade() -> Weight {
273			let mut translated = 0u64;
274			BondedPools::<T>::translate::<V7BondedPoolInner<T>, _>(|_key, old_value| {
275				translated.saturating_inc();
276				Some(old_value.migrate_to_v8())
277			});
278			T::DbWeight::get().reads_writes(translated, translated + 1)
279		}
280
281		#[cfg(feature = "try-runtime")]
282		fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
283			// Check new `claim_permission` field is present.
284			ensure!(
285				BondedPools::<T>::iter()
286					.all(|(_, inner)| inner.commission.claim_permission.is_none()),
287				"`claim_permission` value has not been set correctly."
288			);
289			Ok(())
290		}
291	}
292}
293
294/// This migration accumulates and initializes the [`TotalValueLocked`] for all pools.
295///
296/// WARNING: This migration works under the assumption that the [`BondedPools`] cannot be inflated
297/// arbitrarily. Otherwise this migration could fail due to too high weight.
298pub(crate) mod v7 {
299	use super::*;
300
301	#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Clone)]
302	#[codec(mel_bound(T: Config))]
303	#[scale_info(skip_type_params(T))]
304	pub struct V7Commission<T: Config> {
305		pub current: Option<(Perbill, T::AccountId)>,
306		pub max: Option<Perbill>,
307		pub change_rate: Option<CommissionChangeRate<BlockNumberFor<T>>>,
308		pub throttle_from: Option<BlockNumberFor<T>>,
309	}
310
311	#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, DebugNoBound, PartialEq, Clone)]
312	#[codec(mel_bound(T: Config))]
313	#[scale_info(skip_type_params(T))]
314	pub struct V7BondedPoolInner<T: Config> {
315		pub commission: V7Commission<T>,
316		pub member_counter: u32,
317		pub points: BalanceOf<T>,
318		pub roles: PoolRoles<T::AccountId>,
319		pub state: PoolState,
320	}
321
322	#[allow(dead_code)]
323	#[derive(RuntimeDebugNoBound)]
324	#[cfg_attr(feature = "std", derive(Clone, PartialEq))]
325	pub struct V7BondedPool<T: Config> {
326		/// The identifier of the pool.
327		id: PoolId,
328		/// The inner fields.
329		inner: V7BondedPoolInner<T>,
330	}
331
332	impl<T: Config> V7BondedPool<T> {
333		#[allow(dead_code)]
334		fn bonded_account(&self) -> T::AccountId {
335			Pallet::<T>::generate_bonded_account(self.id)
336		}
337	}
338
339	// NOTE: We cannot put a V7 prefix here since that would change the storage key.
340	#[frame_support::storage_alias]
341	pub type BondedPools<T: Config> =
342		CountedStorageMap<Pallet<T>, Twox64Concat, PoolId, V7BondedPoolInner<T>>;
343
344	pub struct VersionUncheckedMigrateV6ToV7<T>(core::marker::PhantomData<T>);
345	impl<T: Config> UncheckedOnRuntimeUpgrade for VersionUncheckedMigrateV6ToV7<T> {
346		fn on_runtime_upgrade() -> Weight {
347			let migrated = BondedPools::<T>::count();
348			// The TVL should be the sum of all the funds that are actively staked and in the
349			// unbonding process of the account of each pool.
350			let tvl: BalanceOf<T> = helpers::calculate_tvl_by_total_stake::<T>();
351
352			TotalValueLocked::<T>::set(tvl);
353
354			log!(info, "Upgraded {} pools with a TVL of {:?}", migrated, tvl);
355
356			// reads: migrated * (BondedPools +  Staking::total_stake) + count + onchain
357			// version
358			//
359			// writes: current version + TVL
360			T::DbWeight::get().reads_writes(migrated.saturating_mul(2).saturating_add(2).into(), 2)
361		}
362
363		#[cfg(feature = "try-runtime")]
364		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
365			Ok(Vec::new())
366		}
367
368		#[cfg(feature = "try-runtime")]
369		fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
370			// check that the `TotalValueLocked` written is actually the sum of `total_stake` of the
371			// `BondedPools``
372			let tvl: BalanceOf<T> = helpers::calculate_tvl_by_total_stake::<T>();
373			ensure!(
374				TotalValueLocked::<T>::get() == tvl,
375				"TVL written is not equal to `Staking::total_stake` of all `BondedPools`."
376			);
377
378			// calculate the sum of `total_balance` of all `PoolMember` as the upper bound for the
379			// `TotalValueLocked`.
380			let total_balance_members: BalanceOf<T> = PoolMembers::<T>::iter()
381				.map(|(_, member)| member.total_balance())
382				.reduce(|acc, total_balance| acc + total_balance)
383				.unwrap_or_default();
384
385			ensure!(
386				TotalValueLocked::<T>::get() <= total_balance_members,
387				"TVL is greater than the balance of all PoolMembers."
388			);
389
390			ensure!(
391				Pallet::<T>::on_chain_storage_version() >= 7,
392				"nomination-pools::migration::v7: wrong storage version"
393			);
394
395			Ok(())
396		}
397	}
398}
399
400mod v6 {
401	use super::*;
402
403	/// This migration would restrict reward account of pools to go below ED by doing a named
404	/// freeze on all the existing pools.
405	pub struct MigrateToV6<T>(core::marker::PhantomData<T>);
406
407	impl<T: Config> MigrateToV6<T> {
408		fn freeze_ed(pool_id: PoolId) -> Result<(), ()> {
409			let reward_acc = Pallet::<T>::generate_reward_account(pool_id);
410			Pallet::<T>::freeze_pool_deposit(&reward_acc).map_err(|e| {
411				log!(error, "Failed to freeze ED for pool {} with error: {:?}", pool_id, e);
412				()
413			})
414		}
415	}
416	impl<T: Config> UncheckedOnRuntimeUpgrade for MigrateToV6<T> {
417		fn on_runtime_upgrade() -> Weight {
418			let mut success = 0u64;
419			let mut fail = 0u64;
420
421			BondedPools::<T>::iter_keys().for_each(|p| {
422				if Self::freeze_ed(p).is_ok() {
423					success.saturating_inc();
424				} else {
425					fail.saturating_inc();
426				}
427			});
428
429			if fail > 0 {
430				log!(error, "Failed to freeze ED for {} pools", fail);
431			} else {
432				log!(info, "Freezing ED succeeded for {} pools", success);
433			}
434
435			let total = success.saturating_add(fail);
436			// freeze_ed = r:2 w:2
437			// reads: (freeze_ed + bonded pool key) * total
438			// writes: freeze_ed * total
439			T::DbWeight::get().reads_writes(3u64.saturating_mul(total), 2u64.saturating_mul(total))
440		}
441
442		#[cfg(feature = "try-runtime")]
443		fn post_upgrade(_data: Vec<u8>) -> Result<(), TryRuntimeError> {
444			// there should be no ED imbalances anymore..
445			Pallet::<T>::check_ed_imbalance().map(|_| ())
446		}
447	}
448}
449pub mod v5 {
450	use super::*;
451
452	#[derive(Decode)]
453	pub struct OldRewardPool<T: Config> {
454		last_recorded_reward_counter: T::RewardCounter,
455		last_recorded_total_payouts: BalanceOf<T>,
456		total_rewards_claimed: BalanceOf<T>,
457	}
458
459	impl<T: Config> OldRewardPool<T> {
460		fn migrate_to_v5(self) -> RewardPool<T> {
461			RewardPool {
462				last_recorded_reward_counter: self.last_recorded_reward_counter,
463				last_recorded_total_payouts: self.last_recorded_total_payouts,
464				total_rewards_claimed: self.total_rewards_claimed,
465				total_commission_pending: Zero::zero(),
466				total_commission_claimed: Zero::zero(),
467			}
468		}
469	}
470
471	/// This migration adds `total_commission_pending` and `total_commission_claimed` field to every
472	/// `RewardPool`, if any.
473	pub struct MigrateToV5<T>(core::marker::PhantomData<T>);
474	impl<T: Config> OnRuntimeUpgrade for MigrateToV5<T> {
475		fn on_runtime_upgrade() -> Weight {
476			let in_code = Pallet::<T>::in_code_storage_version();
477			let onchain = Pallet::<T>::on_chain_storage_version();
478
479			log!(
480				info,
481				"Running migration with in-code storage version {:?} / onchain {:?}",
482				in_code,
483				onchain
484			);
485
486			if in_code == 5 && onchain == 4 {
487				let mut translated = 0u64;
488				RewardPools::<T>::translate::<OldRewardPool<T>, _>(|_id, old_value| {
489					translated.saturating_inc();
490					Some(old_value.migrate_to_v5())
491				});
492
493				in_code.put::<Pallet<T>>();
494				log!(info, "Upgraded {} pools, storage to version {:?}", translated, in_code);
495
496				// reads: translated + onchain version.
497				// writes: translated + current.put.
498				T::DbWeight::get().reads_writes(translated + 1, translated + 1)
499			} else {
500				log!(info, "Migration did not execute. This probably should be removed");
501				T::DbWeight::get().reads(1)
502			}
503		}
504
505		#[cfg(feature = "try-runtime")]
506		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
507			let rpool_keys = RewardPools::<T>::iter_keys().count();
508			let rpool_values = RewardPools::<T>::iter_values().count();
509			if rpool_keys != rpool_values {
510				log!(info, "๐Ÿ”ฅ There are {} undecodable RewardPools in storage. This migration will try to correct them. keys: {}, values: {}", rpool_keys.saturating_sub(rpool_values), rpool_keys, rpool_values);
511			}
512
513			ensure!(
514				PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
515				"There are undecodable PoolMembers in storage. This migration will not fix that."
516			);
517			ensure!(
518				BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
519				"There are undecodable BondedPools in storage. This migration will not fix that."
520			);
521			ensure!(
522				SubPoolsStorage::<T>::iter_keys().count() ==
523					SubPoolsStorage::<T>::iter_values().count(),
524				"There are undecodable SubPools in storage. This migration will not fix that."
525			);
526			ensure!(
527				Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
528				"There are undecodable Metadata in storage. This migration will not fix that."
529			);
530
531			Ok((rpool_values as u64).encode())
532		}
533
534		#[cfg(feature = "try-runtime")]
535		fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
536			let old_rpool_values: u64 = Decode::decode(&mut &data[..]).unwrap();
537			let rpool_keys = RewardPools::<T>::iter_keys().count() as u64;
538			let rpool_values = RewardPools::<T>::iter_values().count() as u64;
539			ensure!(
540				rpool_keys == rpool_values,
541				"There are STILL undecodable RewardPools - migration failed"
542			);
543
544			if old_rpool_values != rpool_values {
545				log!(
546					info,
547					"๐ŸŽ‰ Fixed {} undecodable RewardPools.",
548					rpool_values.saturating_sub(old_rpool_values)
549				);
550			}
551
552			// ensure all RewardPools items now contain `total_commission_pending` and
553			// `total_commission_claimed` field.
554			ensure!(
555				RewardPools::<T>::iter().all(|(_, reward_pool)| reward_pool
556					.total_commission_pending >=
557					Zero::zero() && reward_pool
558					.total_commission_claimed >=
559					Zero::zero()),
560				"a commission value has been incorrectly set"
561			);
562			ensure!(
563				Pallet::<T>::on_chain_storage_version() >= 5,
564				"nomination-pools::migration::v5: wrong storage version"
565			);
566
567			// These should not have been touched - just in case.
568			ensure!(
569				PoolMembers::<T>::iter_keys().count() == PoolMembers::<T>::iter_values().count(),
570				"There are undecodable PoolMembers in storage."
571			);
572			ensure!(
573				BondedPools::<T>::iter_keys().count() == BondedPools::<T>::iter_values().count(),
574				"There are undecodable BondedPools in storage."
575			);
576			ensure!(
577				SubPoolsStorage::<T>::iter_keys().count() ==
578					SubPoolsStorage::<T>::iter_values().count(),
579				"There are undecodable SubPools in storage."
580			);
581			ensure!(
582				Metadata::<T>::iter_keys().count() == Metadata::<T>::iter_values().count(),
583				"There are undecodable Metadata in storage."
584			);
585
586			Ok(())
587		}
588	}
589}
590
591pub mod v4 {
592	use super::*;
593
594	#[derive(Decode)]
595	pub struct OldBondedPoolInner<T: Config> {
596		pub points: BalanceOf<T>,
597		pub state: PoolState,
598		pub member_counter: u32,
599		pub roles: PoolRoles<T::AccountId>,
600	}
601
602	impl<T: Config> OldBondedPoolInner<T> {
603		fn migrate_to_v4(self) -> BondedPoolInner<T> {
604			BondedPoolInner {
605				commission: Commission::default(),
606				member_counter: self.member_counter,
607				points: self.points,
608				state: self.state,
609				roles: self.roles,
610			}
611		}
612	}
613
614	/// Migrates from `v3` directly to `v5` to avoid the broken `v4` migration.
615	#[allow(deprecated)]
616	pub type MigrateV3ToV5<T, U> = (v4::MigrateToV4<T, U>, v5::MigrateToV5<T>);
617
618	/// # Warning
619	///
620	/// To avoid mangled storage please use `MigrateV3ToV5` instead.
621	/// See: github.com/paritytech/substrate/pull/13715
622	///
623	/// This migration adds a `commission` field to every `BondedPoolInner`, if
624	/// any.
625	#[deprecated(
626		note = "To avoid mangled storage please use `MigrateV3ToV5` instead. See: github.com/paritytech/substrate/pull/13715"
627	)]
628	pub struct MigrateToV4<T, U>(core::marker::PhantomData<(T, U)>);
629	#[allow(deprecated)]
630	impl<T: Config, U: Get<Perbill>> OnRuntimeUpgrade for MigrateToV4<T, U> {
631		fn on_runtime_upgrade() -> Weight {
632			let current = Pallet::<T>::in_code_storage_version();
633			let onchain = Pallet::<T>::on_chain_storage_version();
634
635			log!(
636				info,
637				"Running migration with in-code storage version {:?} / onchain {:?}",
638				current,
639				onchain
640			);
641
642			if onchain == 3 {
643				log!(warn, "Please run MigrateToV5 immediately after this migration. See github.com/paritytech/substrate/pull/13715");
644				let initial_global_max_commission = U::get();
645				GlobalMaxCommission::<T>::set(Some(initial_global_max_commission));
646				log!(
647					info,
648					"Set initial global max commission to {:?}.",
649					initial_global_max_commission
650				);
651
652				let mut translated = 0u64;
653				BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
654					translated.saturating_inc();
655					Some(old_value.migrate_to_v4())
656				});
657
658				StorageVersion::new(4).put::<Pallet<T>>();
659				log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
660
661				// reads: translated + onchain version.
662				// writes: translated + current.put + initial global commission.
663				T::DbWeight::get().reads_writes(translated + 1, translated + 2)
664			} else {
665				log!(info, "Migration did not execute. This probably should be removed");
666				T::DbWeight::get().reads(1)
667			}
668		}
669
670		#[cfg(feature = "try-runtime")]
671		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
672			Ok(Vec::new())
673		}
674
675		#[cfg(feature = "try-runtime")]
676		fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
677			// ensure all BondedPools items now contain an `inner.commission: Commission` field.
678			ensure!(
679				BondedPools::<T>::iter().all(|(_, inner)|
680					// Check current
681					(inner.commission.current.is_none() ||
682					inner.commission.current.is_some()) &&
683					// Check max
684					(inner.commission.max.is_none() || inner.commission.max.is_some()) &&
685					// Check change_rate
686					(inner.commission.change_rate.is_none() ||
687					inner.commission.change_rate.is_some()) &&
688					// Check throttle_from
689					(inner.commission.throttle_from.is_none() ||
690					inner.commission.throttle_from.is_some())),
691				"a commission value has not been set correctly"
692			);
693			ensure!(
694				GlobalMaxCommission::<T>::get() == Some(U::get()),
695				"global maximum commission error"
696			);
697			ensure!(
698				Pallet::<T>::on_chain_storage_version() >= 4,
699				"nomination-pools::migration::v4: wrong storage version"
700			);
701			Ok(())
702		}
703	}
704}
705
706pub mod v3 {
707	use super::*;
708
709	/// This migration removes stale bonded-pool metadata, if any.
710	pub struct MigrateToV3<T>(core::marker::PhantomData<T>);
711	impl<T: Config> OnRuntimeUpgrade for MigrateToV3<T> {
712		fn on_runtime_upgrade() -> Weight {
713			let current = Pallet::<T>::in_code_storage_version();
714			let onchain = Pallet::<T>::on_chain_storage_version();
715
716			if onchain == 2 {
717				log!(
718					info,
719					"Running migration with in-code storage version {:?} / onchain {:?}",
720					current,
721					onchain
722				);
723
724				let mut metadata_iterated = 0u64;
725				let mut metadata_removed = 0u64;
726				Metadata::<T>::iter_keys()
727					.filter(|id| {
728						metadata_iterated += 1;
729						!BondedPools::<T>::contains_key(&id)
730					})
731					.collect::<Vec<_>>()
732					.into_iter()
733					.for_each(|id| {
734						metadata_removed += 1;
735						Metadata::<T>::remove(&id);
736					});
737				StorageVersion::new(3).put::<Pallet<T>>();
738				// metadata iterated + bonded pools read + a storage version read
739				let total_reads = metadata_iterated * 2 + 1;
740				// metadata removed + a storage version write
741				let total_writes = metadata_removed + 1;
742				T::DbWeight::get().reads_writes(total_reads, total_writes)
743			} else {
744				log!(info, "MigrateToV3 should be removed");
745				T::DbWeight::get().reads(1)
746			}
747		}
748
749		#[cfg(feature = "try-runtime")]
750		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
751			Ok(Vec::new())
752		}
753
754		#[cfg(feature = "try-runtime")]
755		fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
756			ensure!(
757				Metadata::<T>::iter_keys().all(|id| BondedPools::<T>::contains_key(&id)),
758				"not all of the stale metadata has been removed"
759			);
760			ensure!(
761				Pallet::<T>::on_chain_storage_version() >= 3,
762				"nomination-pools::migration::v3: wrong storage version"
763			);
764			Ok(())
765		}
766	}
767}
768
769pub mod v2 {
770	use super::*;
771	use sp_runtime::Perbill;
772
773	#[test]
774	fn migration_assumption_is_correct() {
775		// this migrations cleans all the reward accounts to contain exactly ed, and all members
776		// having no claimable rewards. In this state, all fields of the `RewardPool` and
777		// `member.last_recorded_reward_counter` are all zero.
778		use crate::mock::*;
779		ExtBuilder::default().build_and_execute(|| {
780			let join = |x| {
781				Currency::set_balance(&x, Balances::minimum_balance() + 10);
782				frame_support::assert_ok!(Pools::join(RuntimeOrigin::signed(x), 10, 1));
783			};
784
785			assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 10);
786			assert_eq!(
787				RewardPools::<Runtime>::get(1).unwrap(),
788				RewardPool { ..Default::default() }
789			);
790			assert_eq!(
791				PoolMembers::<Runtime>::get(10).unwrap().last_recorded_reward_counter,
792				Zero::zero()
793			);
794
795			join(20);
796			assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 20);
797			assert_eq!(
798				RewardPools::<Runtime>::get(1).unwrap(),
799				RewardPool { ..Default::default() }
800			);
801			assert_eq!(
802				PoolMembers::<Runtime>::get(10).unwrap().last_recorded_reward_counter,
803				Zero::zero()
804			);
805			assert_eq!(
806				PoolMembers::<Runtime>::get(20).unwrap().last_recorded_reward_counter,
807				Zero::zero()
808			);
809
810			join(30);
811			assert_eq!(BondedPool::<Runtime>::get(1).unwrap().points, 30);
812			assert_eq!(
813				RewardPools::<Runtime>::get(1).unwrap(),
814				RewardPool { ..Default::default() }
815			);
816			assert_eq!(
817				PoolMembers::<Runtime>::get(10).unwrap().last_recorded_reward_counter,
818				Zero::zero()
819			);
820			assert_eq!(
821				PoolMembers::<Runtime>::get(20).unwrap().last_recorded_reward_counter,
822				Zero::zero()
823			);
824			assert_eq!(
825				PoolMembers::<Runtime>::get(30).unwrap().last_recorded_reward_counter,
826				Zero::zero()
827			);
828		});
829	}
830
831	#[derive(Decode)]
832	pub struct OldRewardPool<B> {
833		pub balance: B,
834		pub total_earnings: B,
835		pub points: U256,
836	}
837
838	#[derive(Decode)]
839	pub struct OldPoolMember<T: Config> {
840		pub pool_id: PoolId,
841		pub points: BalanceOf<T>,
842		pub reward_pool_total_earnings: BalanceOf<T>,
843		pub unbonding_eras: BoundedBTreeMap<EraIndex, BalanceOf<T>, T::MaxUnbonding>,
844	}
845
846	/// Migrate the pool reward scheme to the new version, as per
847	/// <https://github.com/paritytech/substrate/pull/11669.>.
848	pub struct MigrateToV2<T>(core::marker::PhantomData<T>);
849	impl<T: Config> MigrateToV2<T> {
850		fn run(current: StorageVersion) -> Weight {
851			let mut reward_pools_translated = 0u64;
852			let mut members_translated = 0u64;
853			// just for logging.
854			let mut total_value_locked = BalanceOf::<T>::zero();
855			let mut total_points_locked = BalanceOf::<T>::zero();
856
857			// store each member of the pool, with their active points. In the process, migrate
858			// their data as well.
859			let mut temp_members = BTreeMap::<PoolId, Vec<(T::AccountId, BalanceOf<T>)>>::new();
860			PoolMembers::<T>::translate::<OldPoolMember<T>, _>(|key, old_member| {
861				let id = old_member.pool_id;
862				temp_members.entry(id).or_default().push((key, old_member.points));
863
864				total_points_locked += old_member.points;
865				members_translated += 1;
866				Some(PoolMember::<T> {
867					last_recorded_reward_counter: Zero::zero(),
868					pool_id: old_member.pool_id,
869					points: old_member.points,
870					unbonding_eras: old_member.unbonding_eras,
871				})
872			});
873
874			// translate all reward pools. In the process, do the last payout as well.
875			RewardPools::<T>::translate::<OldRewardPool<BalanceOf<T>>, _>(
876				|id, _old_reward_pool| {
877					// each pool should have at least one member.
878					let members = match temp_members.get(&id) {
879						Some(x) => x,
880						None => {
881							log!(error, "pool {} has no member! deleting it..", id);
882							return None
883						},
884					};
885					let bonded_pool = match BondedPools::<T>::get(id) {
886						Some(x) => x,
887						None => {
888							log!(error, "pool {} has no bonded pool! deleting it..", id);
889							return None
890						},
891					};
892
893					let accumulated_reward = RewardPool::<T>::current_balance(id);
894					let reward_account = Pallet::<T>::generate_reward_account(id);
895					let mut sum_paid_out = BalanceOf::<T>::zero();
896
897					members
898						.into_iter()
899						.filter_map(|(who, points)| {
900							let bonded_pool = match BondedPool::<T>::get(id) {
901								Some(x) => x,
902								None => {
903									log!(error, "pool {} for member {:?} does not exist!", id, who);
904									return None
905								},
906							};
907
908							total_value_locked += bonded_pool.points_to_balance(*points);
909							let portion = Perbill::from_rational(*points, bonded_pool.points);
910							let last_claim = portion * accumulated_reward;
911
912							log!(
913								debug,
914								"{:?} has {:?} ({:?}) of pool {} with total reward of {:?}",
915								who,
916								portion,
917								last_claim,
918								id,
919								accumulated_reward
920							);
921
922							if last_claim.is_zero() {
923								None
924							} else {
925								Some((who, last_claim))
926							}
927						})
928						.for_each(|(who, last_claim)| {
929							let outcome = T::Currency::transfer(
930								&reward_account,
931								&who,
932								last_claim,
933								Preservation::Preserve,
934							);
935
936							if let Err(reason) = outcome {
937								log!(warn, "last reward claim failed due to {:?}", reason,);
938							} else {
939								sum_paid_out = sum_paid_out.saturating_add(last_claim);
940							}
941
942							Pallet::<T>::deposit_event(Event::<T>::PaidOut {
943								member: who.clone(),
944								pool_id: id,
945								payout: last_claim,
946							});
947						});
948
949					// this can only be because of rounding down, or because the person we
950					// wanted to pay their reward to could not accept it (dust).
951					let leftover = accumulated_reward.saturating_sub(sum_paid_out);
952					if !leftover.is_zero() {
953						// pay it all to depositor.
954						let o = T::Currency::transfer(
955							&reward_account,
956							&bonded_pool.roles.depositor,
957							leftover,
958							Preservation::Preserve,
959						);
960						log!(warn, "paying {:?} leftover to the depositor: {:?}", leftover, o);
961					}
962
963					// finally, migrate the reward pool.
964					reward_pools_translated += 1;
965
966					Some(RewardPool {
967						last_recorded_reward_counter: Zero::zero(),
968						last_recorded_total_payouts: Zero::zero(),
969						total_rewards_claimed: Zero::zero(),
970						total_commission_claimed: Zero::zero(),
971						total_commission_pending: Zero::zero(),
972					})
973				},
974			);
975
976			log!(
977				info,
978				"Upgraded {} members, {} reward pools, TVL {:?} TPL {:?}, storage to version {:?}",
979				members_translated,
980				reward_pools_translated,
981				total_value_locked,
982				total_points_locked,
983				current
984			);
985			current.put::<Pallet<T>>();
986
987			T::DbWeight::get().reads_writes(members_translated + 1, reward_pools_translated + 1)
988		}
989	}
990
991	impl<T: Config> OnRuntimeUpgrade for MigrateToV2<T> {
992		fn on_runtime_upgrade() -> Weight {
993			let current = Pallet::<T>::in_code_storage_version();
994			let onchain = Pallet::<T>::on_chain_storage_version();
995
996			log!(
997				info,
998				"Running migration with in-code storage version {:?} / onchain {:?}",
999				current,
1000				onchain
1001			);
1002
1003			if current == 2 && onchain == 1 {
1004				Self::run(current)
1005			} else {
1006				log!(info, "MigrateToV2 did not executed. This probably should be removed");
1007				T::DbWeight::get().reads(1)
1008			}
1009		}
1010
1011		#[cfg(feature = "try-runtime")]
1012		fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
1013			// all reward accounts must have more than ED.
1014			RewardPools::<T>::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> {
1015				ensure!(
1016					<T::Currency as frame_support::traits::fungible::Inspect<T::AccountId>>::balance(&Pallet::<T>::generate_reward_account(id)) >=
1017						T::Currency::minimum_balance(),
1018					"Reward accounts must have greater balance than ED."
1019				);
1020				Ok(())
1021			})?;
1022
1023			Ok(Vec::new())
1024		}
1025
1026		#[cfg(feature = "try-runtime")]
1027		fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
1028			// new version must be set.
1029			ensure!(
1030				Pallet::<T>::on_chain_storage_version() == 2,
1031				"The onchain version must be updated after the migration."
1032			);
1033
1034			// no reward or bonded pool has been skipped.
1035			ensure!(
1036				RewardPools::<T>::iter().count() as u32 == RewardPools::<T>::count(),
1037				"The count of reward pools must remain the same after the migration."
1038			);
1039			ensure!(
1040				BondedPools::<T>::iter().count() as u32 == BondedPools::<T>::count(),
1041				"The count of reward pools must remain the same after the migration."
1042			);
1043
1044			// all reward pools must have exactly ED in them. This means no reward can be claimed,
1045			// and that setting reward counters all over the board to zero will work henceforth.
1046			RewardPools::<T>::iter().try_for_each(|(id, _)| -> Result<(), TryRuntimeError> {
1047				ensure!(
1048					RewardPool::<T>::current_balance(id) == Zero::zero(),
1049					"Reward pool balance must be zero.",
1050				);
1051				Ok(())
1052			})?;
1053
1054			log!(info, "post upgrade hook for MigrateToV2 executed.");
1055			Ok(())
1056		}
1057	}
1058}
1059
1060pub mod v1 {
1061	use super::*;
1062
1063	#[derive(Decode)]
1064	pub struct OldPoolRoles<AccountId> {
1065		pub depositor: AccountId,
1066		pub root: AccountId,
1067		pub nominator: AccountId,
1068		pub bouncer: AccountId,
1069	}
1070
1071	impl<AccountId> OldPoolRoles<AccountId> {
1072		fn migrate_to_v1(self) -> PoolRoles<AccountId> {
1073			PoolRoles {
1074				depositor: self.depositor,
1075				root: Some(self.root),
1076				nominator: Some(self.nominator),
1077				bouncer: Some(self.bouncer),
1078			}
1079		}
1080	}
1081
1082	#[derive(Decode)]
1083	pub struct OldBondedPoolInner<T: Config> {
1084		pub points: BalanceOf<T>,
1085		pub state: PoolState,
1086		pub member_counter: u32,
1087		pub roles: OldPoolRoles<T::AccountId>,
1088	}
1089
1090	impl<T: Config> OldBondedPoolInner<T> {
1091		fn migrate_to_v1(self) -> BondedPoolInner<T> {
1092			// Note: `commission` field not introduced to `BondedPoolInner` until
1093			// migration 4.
1094			BondedPoolInner {
1095				points: self.points,
1096				commission: Commission::default(),
1097				member_counter: self.member_counter,
1098				state: self.state,
1099				roles: self.roles.migrate_to_v1(),
1100			}
1101		}
1102	}
1103
1104	/// Trivial migration which makes the roles of each pool optional.
1105	///
1106	/// Note: The depositor is not optional since they can never change.
1107	pub struct MigrateToV1<T>(core::marker::PhantomData<T>);
1108	impl<T: Config> OnRuntimeUpgrade for MigrateToV1<T> {
1109		fn on_runtime_upgrade() -> Weight {
1110			let current = Pallet::<T>::in_code_storage_version();
1111			let onchain = Pallet::<T>::on_chain_storage_version();
1112
1113			log!(
1114				info,
1115				"Running migration with in-code storage version {:?} / onchain {:?}",
1116				current,
1117				onchain
1118			);
1119
1120			if current == 1 && onchain == 0 {
1121				// this is safe to execute on any runtime that has a bounded number of pools.
1122				let mut translated = 0u64;
1123				BondedPools::<T>::translate::<OldBondedPoolInner<T>, _>(|_key, old_value| {
1124					translated.saturating_inc();
1125					Some(old_value.migrate_to_v1())
1126				});
1127
1128				current.put::<Pallet<T>>();
1129
1130				log!(info, "Upgraded {} pools, storage to version {:?}", translated, current);
1131
1132				T::DbWeight::get().reads_writes(translated + 1, translated + 1)
1133			} else {
1134				log!(info, "Migration did not executed. This probably should be removed");
1135				T::DbWeight::get().reads(1)
1136			}
1137		}
1138
1139		#[cfg(feature = "try-runtime")]
1140		fn post_upgrade(_: Vec<u8>) -> Result<(), TryRuntimeError> {
1141			// new version must be set.
1142			ensure!(
1143				Pallet::<T>::on_chain_storage_version() == 1,
1144				"The onchain version must be updated after the migration."
1145			);
1146			Pallet::<T>::try_state(frame_system::Pallet::<T>::block_number())?;
1147			Ok(())
1148		}
1149	}
1150}
1151
1152mod helpers {
1153	use super::*;
1154
1155	pub(crate) fn calculate_tvl_by_total_stake<T: Config>() -> BalanceOf<T> {
1156		BondedPools::<T>::iter_keys()
1157			.map(|id| {
1158				T::StakeAdapter::total_stake(Pool::from(Pallet::<T>::generate_bonded_account(id)))
1159			})
1160			.reduce(|acc, total_balance| acc + total_balance)
1161			.unwrap_or_default()
1162	}
1163}