Skip to main content

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