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 astar_primitives::dapp_staking::FIXED_TIER_SLOTS_ARGS;
21use core::marker::PhantomData;
22use frame_support::traits::UncheckedOnRuntimeUpgrade;
23
24#[cfg(feature = "try-runtime")]
25mod try_runtime_imports {
26    pub use sp_runtime::TryRuntimeError;
27}
28
29#[cfg(feature = "try-runtime")]
30use try_runtime_imports::*;
31
32/// Exports for versioned migration `type`s for this pallet.
33pub mod versioned_migrations {
34    use super::*;
35
36    /// Migration V10 to V11 wrapped in a [`frame_support::migrations::VersionedMigration`], ensuring
37    /// the migration is only performed when on-chain version is 10.
38    pub type V10ToV11<T, TierParamsConfig, OldErasVoting, OldErasBnE> =
39        frame_support::migrations::VersionedMigration<
40            10,
41            11,
42            v11::VersionMigrateV10ToV11<T, TierParamsConfig, OldErasVoting, OldErasBnE>,
43            Pallet<T>,
44            <T as frame_system::Config>::DbWeight,
45        >;
46}
47
48/// Configuration for V11 tier parameters
49pub trait TierParamsV11Config {
50    fn reward_portion() -> [Permill; 4];
51    fn slot_distribution() -> [Permill; 4];
52    fn tier_thresholds() -> [TierThreshold; 4];
53    fn slot_number_args() -> (u64, u64);
54    fn tier_rank_multipliers() -> [u32; 4];
55}
56
57pub struct DefaultTierParamsV11;
58impl TierParamsV11Config for DefaultTierParamsV11 {
59    fn reward_portion() -> [Permill; 4] {
60        [
61            Permill::from_percent(0),
62            Permill::from_percent(70),
63            Permill::from_percent(30),
64            Permill::from_percent(0),
65        ]
66    }
67
68    fn slot_distribution() -> [Permill; 4] {
69        [
70            Permill::from_percent(0),
71            Permill::from_parts(375_000), // 37.5%
72            Permill::from_parts(625_000), // 62.5%
73            Permill::from_percent(0),
74        ]
75    }
76
77    fn tier_thresholds() -> [TierThreshold; 4] {
78        [
79            TierThreshold::FixedPercentage {
80                required_percentage: Perbill::from_parts(23_200_000), // 2.32%
81            },
82            TierThreshold::FixedPercentage {
83                required_percentage: Perbill::from_parts(9_300_000), // 0.93%
84            },
85            TierThreshold::FixedPercentage {
86                required_percentage: Perbill::from_parts(3_500_000), // 0.35%
87            },
88            // Tier 3: unreachable dummy
89            TierThreshold::FixedPercentage {
90                required_percentage: Perbill::from_parts(0), // 0%
91            },
92        ]
93    }
94
95    fn slot_number_args() -> (u64, u64) {
96        FIXED_TIER_SLOTS_ARGS
97    }
98
99    fn tier_rank_multipliers() -> [u32; 4] {
100        [0, 24_000, 46_700, 0]
101    }
102}
103
104mod v11 {
105    use super::*;
106
107    pub struct VersionMigrateV10ToV11<T, P, OldErasVoting, OldErasBnE>(
108        PhantomData<(T, P, OldErasVoting, OldErasBnE)>,
109    );
110
111    impl<T: Config, P: TierParamsV11Config, OldErasVoting: Get<u32>, OldErasBnE: Get<u32>>
112        UncheckedOnRuntimeUpgrade for VersionMigrateV10ToV11<T, P, OldErasVoting, OldErasBnE>
113    {
114        fn on_runtime_upgrade() -> Weight {
115            let old_eras_voting = OldErasVoting::get();
116            let old_eras_bne = OldErasBnE::get();
117
118            let mut reads: u64 = 0;
119            let mut writes: u64 = 0;
120
121            // 0. Safety: remove excess dApps if count exceeds new limit
122            let current_integrated_dapps = IntegratedDApps::<T>::count();
123            reads += 1;
124
125            let max_dapps_allowed = T::MaxNumberOfContracts::get();
126
127            if current_integrated_dapps > max_dapps_allowed {
128                log::warn!(
129                    target: LOG_TARGET,
130                    "Safety net triggered: {} dApps exceed limit of {}. Removing {} excess dApps.",
131                    current_integrated_dapps,
132                    max_dapps_allowed,
133                    current_integrated_dapps - max_dapps_allowed
134                );
135
136                let excess = current_integrated_dapps.saturating_sub(max_dapps_allowed);
137                let victims: Vec<_> = IntegratedDApps::<T>::iter()
138                    .take(excess as usize)
139                    .map(|(contract, dapp_info)| (contract, dapp_info.id))
140                    .collect();
141
142                reads += excess as u64;
143
144                for (contract, dapp_id) in victims {
145                    ContractStake::<T>::remove(&dapp_id);
146                    IntegratedDApps::<T>::remove(&contract);
147                    writes += 2;
148
149                    let current_era = ActiveProtocolState::<T>::get().era;
150                    Pallet::<T>::deposit_event(Event::<T>::DAppUnregistered {
151                        smart_contract: contract,
152                        era: current_era,
153                    });
154                    log::info!(
155                        target: LOG_TARGET,
156                        "Safety net removed dApp ID {} (contract: {:?})",
157                        dapp_id,
158                        core::any::type_name::<T::SmartContract>()
159                    );
160                }
161
162                // ActiveProtocolState::get() for era => 1 read (done once for all events)
163                reads += 1;
164            }
165
166            // 1. Migrate StaticTierParams
167            let reward_portion = BoundedVec::<Permill, T::NumberOfTiers>::truncate_from(
168                P::reward_portion().to_vec(),
169            );
170            let slot_distribution = BoundedVec::<Permill, T::NumberOfTiers>::truncate_from(
171                P::slot_distribution().to_vec(),
172            );
173            let tier_thresholds = BoundedVec::<TierThreshold, T::NumberOfTiers>::truncate_from(
174                P::tier_thresholds().to_vec(),
175            );
176            let tier_rank_multipliers = BoundedVec::<u32, T::NumberOfTiers>::truncate_from(
177                P::tier_rank_multipliers().to_vec(),
178            );
179
180            let new_params = TierParameters::<T::NumberOfTiers> {
181                reward_portion,
182                slot_distribution,
183                tier_thresholds,
184                slot_number_args: P::slot_number_args(),
185                tier_rank_multipliers,
186            };
187
188            if !new_params.is_valid() {
189                log::error!(
190                    target: LOG_TARGET,
191                    "New TierParameters validation failed. Enabling maintenance mode."
192                );
193
194                // ActiveProtocolState::mutate => 1 read + 1 write
195                ActiveProtocolState::<T>::mutate(|state| {
196                    state.maintenance = true;
197                });
198                reads += 1;
199                writes += 1;
200
201                return T::DbWeight::get().reads_writes(reads, writes);
202            }
203
204            // StaticTierParams::put => 1 write
205            StaticTierParams::<T>::put(new_params);
206            writes += 1;
207            log::info!(target: LOG_TARGET, "StaticTierParams updated successfully");
208
209            // 2. Update ActiveProtocolState in a SINGLE mutate (avoid extra .get() read)
210            // ActiveProtocolState::mutate => 1 read + 1 write
211            ActiveProtocolState::<T>::mutate(|state| {
212                if state.period_info.subperiod == Subperiod::Voting {
213                    let current_block: u32 =
214                        frame_system::Pallet::<T>::block_number().saturated_into();
215
216                    // Old/new voting period lengths (in blocks)
217                    let old_voting_length: u32 =
218                        old_eras_voting.saturating_mul(T::CycleConfiguration::blocks_per_era());
219                    let new_voting_length: u32 = Pallet::<T>::blocks_per_voting_period()
220                        .max(T::CycleConfiguration::blocks_per_era());
221
222                    // Old schedule
223                    let remaining_old: u32 = state.next_era_start.saturating_sub(current_block);
224                    let elapsed: u32 = old_voting_length.saturating_sub(remaining_old);
225
226                    // New schedule
227                    let remaining_new: u32 = new_voting_length.saturating_sub(elapsed);
228
229                    // If new period has already passed (elapsed >= new_voting_length),
230                    // schedule for next block. Otherwise, use the calculated remainder.
231                    state.next_era_start = if remaining_new == 0 {
232                        current_block.saturating_add(1)
233                    } else {
234                        current_block.saturating_add(remaining_new)
235                    };
236
237                    log::info!(
238                        target: LOG_TARGET,
239                        "ActiveProtocolState updated: next_era_start (old_length={}, new_length={}, elapsed={}, remaining_new={})",
240                        old_voting_length,
241                        new_voting_length,
242                        elapsed,
243                        remaining_new
244                    );
245                }
246                if state.period_info.subperiod == Subperiod::Voting {
247                    // Recalculate next_era_start block
248                    let current_block: u32 =
249                        frame_system::Pallet::<T>::block_number().saturated_into();
250                    let new_voting_length: u32 = Pallet::<T>::blocks_per_voting_period()
251                        .max(T::CycleConfiguration::blocks_per_era());
252                    let remaining_old: u32 = state.next_era_start.saturating_sub(current_block);
253                    // Carry over remaining time, but never extend beyond the new voting length.
254                    // If already overdue, schedule for the next block.
255                    let remaining_new: u32 = remaining_old.min(new_voting_length).max(1);
256
257                    state.next_era_start = current_block.saturating_add(remaining_new);
258
259                    log::info!(
260                        target: LOG_TARGET,
261                        "ActiveProtocolState updated: next_era_start (remaining_old={}, remaining_new={})",
262                        remaining_old,
263                        remaining_new
264                    );
265                } else {
266                    // Build&Earn: adjust remainder for next_subperiod_start_era
267                    let new_eras_total: EraNumber =
268                        T::CycleConfiguration::eras_per_build_and_earn_subperiod();
269
270                    // "only the remainder" logic
271                    let current_era: EraNumber = state.era;
272                    let old_end: EraNumber = state.period_info.next_subperiod_start_era;
273
274                    let remaining_old: EraNumber = old_end.saturating_sub(current_era);
275                    let elapsed: EraNumber = old_eras_bne.saturating_sub(remaining_old);
276
277                    let remaining_new: EraNumber = new_eras_total.saturating_sub(elapsed);
278
279                    state.period_info.next_subperiod_start_era =
280                        current_era.saturating_add(remaining_new);
281
282                    log::info!(
283                        target: LOG_TARGET,
284                        "ActiveProtocolState updated: next_subperiod_start_era (remainder-adjusted)"
285                    );
286                }
287            });
288            reads += 1;
289            writes += 1;
290
291            T::DbWeight::get().reads_writes(reads, writes)
292        }
293
294        #[cfg(feature = "try-runtime")]
295        fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
296            let protocol_state = ActiveProtocolState::<T>::get();
297            let current_block: u32 = frame_system::Pallet::<T>::block_number().saturated_into();
298
299            let pre_dapp_count = IntegratedDApps::<T>::count();
300            let max_allowed = T::MaxNumberOfContracts::get();
301
302            log::info!(
303                target: LOG_TARGET,
304                "Pre-upgrade: dApp count={}, max={}, cleanup_needed={}",
305                pre_dapp_count,
306                max_allowed,
307                pre_dapp_count > max_allowed
308            );
309
310            Ok((
311                protocol_state.period_info.subperiod,
312                protocol_state.era,
313                protocol_state.next_era_start,
314                protocol_state.period_info.next_subperiod_start_era,
315                current_block,
316                pre_dapp_count,
317                max_allowed,
318            )
319                .encode())
320        }
321
322        #[cfg(feature = "try-runtime")]
323        fn post_upgrade(data: Vec<u8>) -> Result<(), TryRuntimeError> {
324            let (
325                old_subperiod,
326                old_era,
327                old_next_era_start,
328                old_next_subperiod_era,
329                pre_block,
330                pre_dapp_count,
331                max_allowed,
332            ): (Subperiod, EraNumber, u32, EraNumber, u32, u32, u32) =
333                Decode::decode(&mut &data[..])
334                    .map_err(|_| TryRuntimeError::Other("Failed to decode pre-upgrade data"))?;
335            let old_eras_voting = OldErasVoting::get();
336            let old_eras_bne = OldErasBnE::get();
337
338            // Verify storage version
339            ensure!(
340                Pallet::<T>::on_chain_storage_version() == StorageVersion::new(11),
341                "Storage version should be 11"
342            );
343
344            // 1. Verify cleanup worked
345            let post_dapp_count = IntegratedDApps::<T>::count();
346            log::debug!(
347                "post_dapp_count={}, max_allowed={}",
348                post_dapp_count,
349                max_allowed
350            );
351            ensure!(
352                post_dapp_count <= max_allowed,
353                "dApp count still exceeds limit",
354            );
355
356            if pre_dapp_count > max_allowed {
357                let expected_removed = pre_dapp_count - max_allowed;
358                let actual_removed = pre_dapp_count - post_dapp_count;
359                log::debug!(
360                    "Removed {} dApps, expected to remove {}",
361                    actual_removed,
362                    expected_removed
363                );
364                ensure!(
365                    actual_removed == expected_removed,
366                    "Mismatch in the expected dApps to be unregistered"
367                );
368            }
369
370            // 2. Verify new StaticTierParams are valid
371            let new_params = StaticTierParams::<T>::get();
372            ensure!(new_params.is_valid(), "New tier params invalid");
373            ensure!(
374                new_params.reward_portion.as_slice() == P::reward_portion(),
375                "reward_portion mismatch"
376            );
377            ensure!(
378                new_params.tier_rank_multipliers.as_slice() == P::tier_rank_multipliers(),
379                "tier_rank_multipliers mismatch"
380            );
381
382            // 3. Verify ActiveProtocolState update
383            let protocol_state = ActiveProtocolState::<T>::get();
384            ensure!(!protocol_state.maintenance, "Maintenance mode enabled");
385
386            if old_subperiod == Subperiod::Voting {
387                let old_voting_length: u32 =
388                    old_eras_voting.saturating_mul(T::CycleConfiguration::blocks_per_era());
389                let new_voting_length: u32 = Pallet::<T>::blocks_per_voting_period();
390
391                let remaining_old: u32 = old_next_era_start.saturating_sub(pre_block);
392                let elapsed: u32 = old_voting_length.saturating_sub(remaining_old);
393                let remaining_new: u32 = new_voting_length.saturating_sub(elapsed);
394
395                let expected = if remaining_new == 0 {
396                    pre_block.saturating_add(1)
397                } else {
398                    pre_block.saturating_add(remaining_new)
399                };
400
401                ensure!(
402                    protocol_state.next_era_start == expected,
403                    "Voting: next_era_start incorrect"
404                );
405            } else {
406                let new_total: EraNumber =
407                    T::CycleConfiguration::eras_per_build_and_earn_subperiod();
408                let remaining_old: EraNumber = old_next_subperiod_era.saturating_sub(old_era);
409                let elapsed: EraNumber = old_eras_bne.saturating_sub(remaining_old);
410                let remaining_new: EraNumber = new_total.saturating_sub(elapsed);
411                let expected: EraNumber = old_era.saturating_add(remaining_new);
412
413                ensure!(
414                    protocol_state.period_info.next_subperiod_start_era == expected,
415                    "BuildEarn: next_subperiod_start_era incorrect"
416                );
417                ensure!(
418                    old_next_era_start > expected,
419                    "next_era_start did not update as expected"
420                );
421            }
422
423            log::info!(target: LOG_TARGET, "Post-upgrade: All checks passed");
424            Ok(())
425        }
426    }
427}
428
429mod v10 {
430    use super::*;
431    use frame_support::storage_alias;
432
433    /// v10 TierParameters (without tier_rank_multipliers)
434    #[derive(Encode, Decode, Clone)]
435    pub struct TierParameters<NT: Get<u32>> {
436        pub reward_portion: BoundedVec<Permill, NT>,
437        pub slot_distribution: BoundedVec<Permill, NT>,
438        pub tier_thresholds: BoundedVec<TierThreshold, NT>,
439        pub slot_number_args: (u64, u64),
440    }
441
442    #[storage_alias]
443    pub type StaticTierParams<T: Config> =
444        StorageValue<Pallet<T>, TierParameters<<T as Config>::NumberOfTiers>, OptionQuery>;
445}