pallet_dapp_staking/
migration.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19use super::*;
20use core::marker::PhantomData;
21use frame_support::{
22    migration::clear_storage_prefix,
23    migrations::{MigrationId, SteppedMigration, SteppedMigrationError},
24    traits::{GetStorageVersion, OnRuntimeUpgrade, UncheckedOnRuntimeUpgrade},
25    weights::WeightMeter,
26};
27
28#[cfg(feature = "try-runtime")]
29use sp_std::vec::Vec;
30
31#[cfg(feature = "try-runtime")]
32use sp_runtime::TryRuntimeError;
33
34/// Exports for versioned migration `type`s for this pallet.
35pub mod versioned_migrations {
36    use super::*;
37
38    /// Migration V9 to V10 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring
39    /// the migration is only performed when on-chain version is 9.
40    pub type V9ToV10<T, MaxPercentages> = frame_support::migrations::VersionedMigration<
41        9,
42        10,
43        v10::VersionMigrateV9ToV10<T, MaxPercentages>,
44        Pallet<T>,
45        <T as frame_system::Config>::DbWeight,
46    >;
47}
48
49mod v10 {
50    use super::*;
51    use crate::migration::v9::{
52        TierParameters as TierParametersV9, TierThreshold as TierThresholdV9,
53    };
54
55    pub struct VersionMigrateV9ToV10<T, MaxPercentages>(PhantomData<(T, MaxPercentages)>);
56
57    impl<T: Config, MaxPercentages: Get<[Option<Perbill>; 4]>> UncheckedOnRuntimeUpgrade
58        for VersionMigrateV9ToV10<T, MaxPercentages>
59    {
60        fn on_runtime_upgrade() -> Weight {
61            let max_percentages = MaxPercentages::get();
62
63            // Update static tier parameters with new max thresholds from the runtime configurable param TierThresholds
64            let result = StaticTierParams::<T>::translate::<TierParametersV9<T::NumberOfTiers>, _>(
65                |maybe_old_params| match maybe_old_params {
66                    Some(old_params) => {
67                        let new_tier_thresholds: Vec<TierThreshold> = old_params
68                            .tier_thresholds
69                            .iter()
70                            .enumerate()
71                            .map(|(idx, old_threshold)| {
72                                let maximum_percentage = if idx < max_percentages.len() {
73                                    max_percentages[idx]
74                                } else {
75                                    None
76                                };
77                                map_threshold(old_threshold, maximum_percentage)
78                            })
79                            .collect();
80
81                        let tier_thresholds =
82                            BoundedVec::<TierThreshold, T::NumberOfTiers>::try_from(
83                                new_tier_thresholds,
84                            );
85
86                        match tier_thresholds {
87                            Ok(tier_thresholds) => Some(TierParameters {
88                                slot_distribution: old_params.slot_distribution,
89                                reward_portion: old_params.reward_portion,
90                                tier_thresholds,
91                                slot_number_args: old_params.slot_number_args,
92                            }),
93                            Err(err) => {
94                                log::error!(
95                                    "Failed to convert TierThresholds parameters: {:?}",
96                                    err
97                                );
98                                None
99                            }
100                        }
101                    }
102                    _ => None,
103                },
104            );
105
106            if result.is_err() {
107                log::error!("Failed to translate StaticTierParams from previous V9 type to current V10 type. Check TierParametersV9 decoding.");
108                // Enable maintenance mode.
109                ActiveProtocolState::<T>::mutate(|state| {
110                    state.maintenance = true;
111                });
112                log::warn!("Maintenance mode enabled.");
113                return T::DbWeight::get().reads_writes(1, 0);
114            }
115
116            T::DbWeight::get().reads_writes(1, 1)
117        }
118
119        #[cfg(feature = "try-runtime")]
120        fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
121            let old_params = v9::StaticTierParams::<T>::get().ok_or_else(|| {
122                TryRuntimeError::Other(
123                    "dapp-staking-v3::migration::v10: No old params found for StaticTierParams",
124                )
125            })?;
126            Ok(old_params.encode())
127        }
128
129        #[cfg(feature = "try-runtime")]
130        fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
131            // Decode the old values
132            let old_params: TierParametersV9<T::NumberOfTiers> = Decode::decode(&mut &data[..])
133                .map_err(|_| {
134                    TryRuntimeError::Other(
135                        "dapp-staking-v3::migration::v10: Failed to decode old values",
136                    )
137                })?;
138
139            // Get the new values
140            let new_config = TierConfig::<T>::get();
141            let new_params = StaticTierParams::<T>::get();
142
143            // Verify that new params and new config are valid
144            assert!(new_params.is_valid());
145            assert!(new_config.is_valid());
146
147            // Verify parameters remain unchanged
148            assert_eq!(
149                old_params.slot_distribution, new_params.slot_distribution,
150                "dapp-staking-v3::migration::v10: Slot distribution has changed"
151            );
152            assert_eq!(
153                old_params.reward_portion, new_params.reward_portion,
154                "dapp-staking-v3::migration::v10: Reward portion has changed"
155            );
156            assert_eq!(
157                old_params.tier_thresholds.len(),
158                new_params.tier_thresholds.len(),
159                "dapp-staking-v3::migration::v10: Number of tier thresholds has changed"
160            );
161
162            for (_, (old_threshold, new_threshold)) in old_params
163                .tier_thresholds
164                .iter()
165                .zip(new_params.tier_thresholds.iter())
166                .enumerate()
167            {
168                match (old_threshold, new_threshold) {
169                    (
170                        TierThresholdV9::FixedPercentage {
171                            required_percentage: old_req,
172                        },
173                        TierThreshold::FixedPercentage {
174                            required_percentage: new_req,
175                        },
176                    ) => {
177                        assert_eq!(
178                            old_req, new_req,
179                            "dapp-staking-v3::migration::v10: Fixed percentage changed",
180                        );
181                    }
182                    (
183                        TierThresholdV9::DynamicPercentage {
184                            percentage: old_percentage,
185                            minimum_required_percentage: old_min,
186                        },
187                        TierThreshold::DynamicPercentage {
188                            percentage: new_percentage,
189                            minimum_required_percentage: new_min,
190                            maximum_possible_percentage: _, // We don't verify this as it's new
191                        },
192                    ) => {
193                        assert_eq!(
194                            old_percentage, new_percentage,
195                            "dapp-staking-v3::migration::v10: Percentage changed"
196                        );
197                        assert_eq!(
198                            old_min, new_min,
199                            "dapp-staking-v3::migration::v10: Minimum percentage changed"
200                        );
201                    }
202                    _ => {
203                        return Err(TryRuntimeError::Other(
204                            "dapp-staking-v3::migration::v10: Tier threshold type mismatch",
205                        ));
206                    }
207                }
208            }
209
210            let expected_max_percentages = MaxPercentages::get();
211            for (idx, tier_threshold) in new_params.tier_thresholds.iter().enumerate() {
212                if let TierThreshold::DynamicPercentage {
213                    maximum_possible_percentage,
214                    ..
215                } = tier_threshold
216                {
217                    let expected_maximum_percentage = if idx < expected_max_percentages.len() {
218                        expected_max_percentages[idx]
219                    } else {
220                        None
221                    }
222                    .unwrap_or(Perbill::from_percent(100));
223                    assert_eq!(
224                        *maximum_possible_percentage, expected_maximum_percentage,
225                        "dapp-staking-v3::migration::v10: Max percentage differs from expected",
226                    );
227                }
228            }
229
230            // Verify storage version has been updated
231            ensure!(
232                Pallet::<T>::on_chain_storage_version() >= 10,
233                "dapp-staking-v3::migration::v10: Wrong storage version."
234            );
235
236            Ok(())
237        }
238    }
239
240    pub fn map_threshold(old: &TierThresholdV9, max_percentage: Option<Perbill>) -> TierThreshold {
241        match old {
242            TierThresholdV9::FixedPercentage {
243                required_percentage,
244            } => TierThreshold::FixedPercentage {
245                required_percentage: *required_percentage,
246            },
247            TierThresholdV9::DynamicPercentage {
248                percentage,
249                minimum_required_percentage,
250            } => TierThreshold::DynamicPercentage {
251                percentage: *percentage,
252                minimum_required_percentage: *minimum_required_percentage,
253                maximum_possible_percentage: max_percentage.unwrap_or(Perbill::from_percent(100)), // Default to 100% if not specified,
254            },
255        }
256    }
257}
258
259mod v9 {
260    use super::*;
261    use frame_support::storage_alias;
262
263    #[derive(Encode, Decode)]
264    pub struct TierParameters<NT: Get<u32>> {
265        pub reward_portion: BoundedVec<Permill, NT>,
266        pub slot_distribution: BoundedVec<Permill, NT>,
267        pub tier_thresholds: BoundedVec<TierThreshold, NT>,
268        pub slot_number_args: (u64, u64),
269    }
270
271    #[derive(Encode, Decode)]
272    pub enum TierThreshold {
273        FixedPercentage {
274            required_percentage: Perbill,
275        },
276        DynamicPercentage {
277            percentage: Perbill,
278            minimum_required_percentage: Perbill,
279        },
280    }
281
282    /// v9 type for [`crate::StaticTierParams`]
283    #[storage_alias]
284    pub type StaticTierParams<T: Config> =
285        StorageValue<Pallet<T>, TierParameters<<T as Config>::NumberOfTiers>>;
286}
287
288const PALLET_MIGRATIONS_ID: &[u8; 16] = b"dapp-staking-mbm";
289
290pub struct LazyMigration<T, W: WeightInfo>(PhantomData<(T, W)>);
291
292impl<T: Config, W: WeightInfo> SteppedMigration for LazyMigration<T, W> {
293    type Cursor = <T as frame_system::Config>::AccountId;
294    // Without the explicit length here the construction of the ID would not be infallible.
295    type Identifier = MigrationId<16>;
296
297    /// The identifier of this migration. Which should be globally unique.
298    fn id() -> Self::Identifier {
299        MigrationId {
300            pallet_id: *PALLET_MIGRATIONS_ID,
301            version_from: 0,
302            version_to: 1,
303        }
304    }
305
306    fn step(
307        mut cursor: Option<Self::Cursor>,
308        meter: &mut WeightMeter,
309    ) -> Result<Option<Self::Cursor>, SteppedMigrationError> {
310        let required = W::step();
311        // If there is not enough weight for a single step, return an error. This case can be
312        // problematic if it is the first migration that ran in this block. But there is nothing
313        // that we can do about it here.
314        if meter.remaining().any_lt(required) {
315            return Err(SteppedMigrationError::InsufficientWeight { required });
316        }
317
318        let mut count = 0u32;
319        let mut migrated = 0u32;
320        let current_block_number =
321            frame_system::Pallet::<T>::block_number().saturated_into::<u32>();
322
323        // We loop here to do as much progress as possible per step.
324        loop {
325            if meter.try_consume(required).is_err() {
326                break;
327            }
328
329            let mut iter = if let Some(last_key) = cursor {
330                // If a cursor is provided, start iterating from the stored value
331                // corresponding to the last key processed in the previous step.
332                // Note that this only works if the old and the new map use the same way to hash
333                // storage keys.
334                Ledger::<T>::iter_from(Ledger::<T>::hashed_key_for(last_key))
335            } else {
336                // If no cursor is provided, start iterating from the beginning.
337                Ledger::<T>::iter()
338            };
339
340            // If there's a next item in the iterator, perform the migration.
341            if let Some((ref last_key, mut ledger)) = iter.next() {
342                // inc count
343                count.saturating_inc();
344
345                if ledger.unlocking.is_empty() {
346                    // no unlocking for this account, nothing to update
347                    // Return the processed key as the new cursor.
348                    cursor = Some(last_key.clone());
349                    continue;
350                }
351                for chunk in ledger.unlocking.iter_mut() {
352                    if current_block_number >= chunk.unlock_block {
353                        continue; // chunk already unlocked
354                    }
355                    let remaining_blocks = chunk.unlock_block.saturating_sub(current_block_number);
356                    chunk.unlock_block.saturating_accrue(remaining_blocks);
357                }
358
359                // Override ledger
360                Ledger::<T>::insert(last_key, ledger);
361
362                // inc migrated
363                migrated.saturating_inc();
364
365                // Return the processed key as the new cursor.
366                cursor = Some(last_key.clone())
367            } else {
368                // Signal that the migration is complete (no more items to process).
369                cursor = None;
370                break;
371            }
372        }
373        log::info!(target: LOG_TARGET, "🚚 iterated {count} entries, migrated {migrated}");
374        Ok(cursor)
375    }
376}
377
378/// Double the remaining block for next era start
379pub struct AdjustEraMigration<T>(PhantomData<T>);
380
381impl<T: Config> OnRuntimeUpgrade for AdjustEraMigration<T> {
382    fn on_runtime_upgrade() -> Weight {
383        log::info!("🚚 migrated to async backing, adjust next era start");
384        ActiveProtocolState::<T>::mutate_exists(|maybe| {
385            if let Some(state) = maybe {
386                let current_block_number =
387                    frame_system::Pallet::<T>::block_number().saturated_into::<u32>();
388                let remaining = state.next_era_start.saturating_sub(current_block_number);
389                state.next_era_start.saturating_accrue(remaining);
390            }
391        });
392        T::DbWeight::get().reads_writes(1, 1)
393    }
394}
395
396pub const EXPECTED_PALLET_DAPP_STAKING_VERSION: u16 = 9;
397
398pub struct DappStakingCleanupMigration<T>(PhantomData<T>);
399impl<T: Config> OnRuntimeUpgrade for DappStakingCleanupMigration<T> {
400    fn on_runtime_upgrade() -> Weight {
401        let dapp_staking_storage_version =
402            <Pallet<T> as GetStorageVersion>::on_chain_storage_version();
403        if dapp_staking_storage_version != EXPECTED_PALLET_DAPP_STAKING_VERSION {
404            log::info!("Aborting migration due to unexpected on-chain storage versions for pallet-dapp-staking: {:?}. Expectation was: {:?}.", dapp_staking_storage_version, EXPECTED_PALLET_DAPP_STAKING_VERSION);
405            return T::DbWeight::get().reads(1);
406        }
407
408        let pallet_prefix: &[u8] = b"DappStaking";
409        let result =
410            clear_storage_prefix(pallet_prefix, b"ActiveBonusUpdateState", &[], None, None);
411        log::info!(
412            "cleanup dAppStaking migration result: {:?}",
413            result.deconstruct()
414        );
415
416        T::DbWeight::get().reads_writes(1, 1)
417    }
418}