referrerpolicy=no-referrer-when-downgrade

pallet_staking_async/
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//! Storage migrations for the staking-async pallet.
19
20use crate::{log, reward::EraRewardManager, Config, DisableMintingGuard, RewardKind, RewardPot};
21use frame_support::{
22	pallet_prelude::*,
23	traits::{
24		fungible::{Inspect, Mutate},
25		tokens::Preservation,
26		Get, OnRuntimeUpgrade,
27	},
28	PalletId,
29};
30use sp_runtime::{traits::AccountIdConversion, Saturating};
31use sp_staking::EraIndex;
32
33/// One-shot migration relocating already-funded era pots after the seed-derivation
34/// change (#11930) so existing rewards stay claimable. For runtimes that activated
35/// DAP before the slot-based rotation of era pot accounts landed.
36///
37/// Migrates a single [`RewardKind`] per instance — list it twice in `Migrations`
38/// if both kinds need migrating.
39///
40/// Idempotent: skips eras whose old account has no balance.
41///
42/// Generic params:
43/// - `T`: pallet config.
44/// - `S`: same `Get<PalletId>` used by [`crate::Seed`] to derive pot accounts.
45/// - `K`: which [`RewardKind`] to migrate.
46pub struct MigrateEraPotsToPool<T, S, K>(core::marker::PhantomData<(T, S, K)>);
47
48impl<T: Config, S: Get<PalletId>, K: Get<RewardKind>> MigrateEraPotsToPool<T, S, K> {
49	/// Reproduces the historical seed derivation used before the slot-based
50	/// rotation, needed to locate pre-migration balances.
51	fn old_pot_account(era: EraIndex) -> T::AccountId {
52		S::get().into_sub_account_truncating(RewardPot::Era(era, K::get()))
53	}
54}
55
56impl<T: Config, S: Get<PalletId>, K: Get<RewardKind>> OnRuntimeUpgrade
57	for MigrateEraPotsToPool<T, S, K>
58{
59	fn on_runtime_upgrade() -> Weight {
60		let mut weight = T::DbWeight::get().reads(2);
61
62		let Some(guard_era) = DisableMintingGuard::<T>::get() else {
63			log!(info, "EraPotsToPool: guard unset, nothing to migrate");
64			return weight;
65		};
66
67		let active_era_idx = crate::session_rotation::Rotator::<T>::active_era();
68		debug_assert!(
69			active_era_idx >= guard_era,
70			"active_era should always be past DisableMintingGuard once set"
71		);
72		if active_era_idx <= guard_era {
73			return weight;
74		}
75
76		// Anything older than `HistoryDepth` was already cleaned up via the
77		// normal payout flow.
78		let oldest = active_era_idx.saturating_sub(T::HistoryDepth::get()).max(guard_era);
79
80		let kind = K::get();
81		let mut migrated = 0u32;
82		for era in oldest..active_era_idx {
83			let old = Self::old_pot_account(era);
84			weight.saturating_accrue(T::DbWeight::get().reads(1));
85			if frame_system::Pallet::<T>::providers(&old) == 0 {
86				continue;
87			}
88
89			// `create` is idempotent: increments the provider on the new
90			// slot account only if not already provided.
91			let new = EraRewardManager::<T>::create(era, kind);
92			weight.saturating_accrue(T::DbWeight::get().reads_writes(1, 1));
93
94			let balance = T::Currency::balance(&old);
95			weight.saturating_accrue(T::DbWeight::get().reads(1));
96			if !balance.is_zero() {
97				if let Err(e) = T::Currency::transfer(&old, &new, balance, Preservation::Expendable)
98				{
99					log!(
100						error,
101						"EraPotsToPool: era {} kind {:?}: transfer failed: {:?}",
102						era,
103						kind,
104						e,
105					);
106					// Keep providers on the old account; balance is still there
107					// and the account remains queryable for manual recovery.
108					continue;
109				}
110				weight.saturating_accrue(T::DbWeight::get().reads_writes(2, 2));
111			}
112
113			// Try to release the old drained account so it can be reaped.
114			let _ = frame_system::Pallet::<T>::dec_providers(&old);
115			weight.saturating_accrue(T::DbWeight::get().writes(1));
116			migrated.saturating_accrue(1);
117		}
118
119		log!(
120			info,
121			"EraPotsToPool: migrated {} eras of kind {:?} from guard {} to active {}",
122			migrated,
123			kind,
124			guard_era,
125			active_era_idx,
126		);
127		weight
128	}
129
130	#[cfg(feature = "try-runtime")]
131	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
132		use crate::{BalanceOf, PotAccountProvider};
133		use codec::Encode;
134		use sp_runtime::traits::Zero;
135
136		let kind = K::get();
137		let mut total_old: BalanceOf<T> = Zero::zero();
138		let mut total_new_pre: BalanceOf<T> = Zero::zero();
139		for era in Self::migrated_eras() {
140			let old = Self::old_pot_account(era);
141			total_old.saturating_accrue(T::Currency::balance(&old));
142			let new = T::RewardPots::pot_account(RewardPot::Era(era, kind));
143			total_new_pre.saturating_accrue(T::Currency::balance(&new));
144		}
145		Ok((total_old, total_new_pre).encode())
146	}
147
148	#[cfg(feature = "try-runtime")]
149	fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
150		use crate::{BalanceOf, PotAccountProvider};
151		use codec::Decode;
152		use sp_runtime::traits::Zero;
153
154		let (total_old, total_new_pre): (BalanceOf<T>, BalanceOf<T>) =
155			Decode::decode(&mut &state[..]).map_err(|_| "decode pre_upgrade state")?;
156
157		let kind = K::get();
158		let mut remaining_old: BalanceOf<T> = Zero::zero();
159		let mut total_new_post: BalanceOf<T> = Zero::zero();
160		for era in Self::migrated_eras() {
161			let old = Self::old_pot_account(era);
162			remaining_old.saturating_accrue(T::Currency::balance(&old));
163			let new = T::RewardPots::pot_account(RewardPot::Era(era, kind));
164			total_new_post.saturating_accrue(T::Currency::balance(&new));
165		}
166
167		frame_support::ensure!(
168			remaining_old.is_zero(),
169			"old pot accounts still hold balance after migration"
170		);
171		// Funds must have landed in the new pots, accounting for whatever was
172		// already there pre-migration (if anything).
173		frame_support::ensure!(
174			total_new_post.saturating_sub(total_new_pre) == total_old,
175			"new pot balances did not increase by total_old after migration"
176		);
177		Ok(())
178	}
179}
180
181#[cfg(feature = "try-runtime")]
182impl<T: Config, S: Get<PalletId>, K: Get<RewardKind>> MigrateEraPotsToPool<T, S, K> {
183	/// Returns the eras the migration touches. Only used for pre/post state checks.
184	fn migrated_eras() -> core::ops::Range<EraIndex> {
185		let active = crate::session_rotation::Rotator::<T>::active_era();
186		match DisableMintingGuard::<T>::get() {
187			Some(guard) if active > guard => {
188				let oldest = active.saturating_sub(T::HistoryDepth::get()).max(guard);
189				oldest..active
190			},
191			_ => 0..0,
192		}
193	}
194}