referrerpolicy=no-referrer-when-downgrade

pallet_dap/
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//! DAP pallet migrations.
19
20use super::*;
21use crate::weights::WeightInfo;
22use frame_support::traits::{Time, UncheckedOnRuntimeUpgrade};
23
24/// V1 to V2 migration: seeds `BudgetAllocation`, credits a one-shot catch-up drip
25/// for the window `[P::get(), now]`, and initializes `LastIssuanceTimestamp` to
26/// `now` so regular drips start a fresh cadence from here.
27///
28/// - `T`: DAP pallet config
29/// - `P`: `Get<u64>` providing the last inflation timestamp before DAP activation (e.g.
30///   `ActiveEra.start` from staking). Only used as an input to the catch-up drip — not persisted.
31/// - `B`: `Get<BudgetAllocationMap>` providing the initial budget allocation.
32/// - `M`: `Get<u64>` providing the maximum elapsed window (ms) the catch-up is allowed to credit.
33///   Should usually be max staking era length.
34///
35/// Idempotent: the catch-up is skipped if `LastIssuanceTimestamp` is already
36/// non-zero, so a re-entry does not double-credit.
37///
38/// The catch-up drip bypasses `MaxElapsedPerDrip` but is clamped by `M` as the
39/// one-shot safety ceiling.
40pub type MigrateV1ToV2<T, P, B, M> = frame_support::migrations::VersionedMigration<
41	1,
42	2,
43	InnerMigrateV1ToV2<T, P, B, M>,
44	pallet::Pallet<T>,
45	<T as frame_system::Config>::DbWeight,
46>;
47
48/// Inner (unversioned) migration logic. Use [`MigrateV1ToV2`] instead.
49pub struct InnerMigrateV1ToV2<T, P, B, M>(core::marker::PhantomData<(T, P, B, M)>);
50
51impl<T: Config, P: Get<u64>, B: Get<BudgetAllocationMap>, M: Get<u64>> UncheckedOnRuntimeUpgrade
52	for InnerMigrateV1ToV2<T, P, B, M>
53{
54	fn on_runtime_upgrade() -> frame_support::weights::Weight {
55		let mut weight = T::DbWeight::get().reads(3);
56
57		// Seed BudgetAllocation first so the catch-up drip has recipients to distribute to.
58		let current_budget = BudgetAllocation::<T>::get();
59		if current_budget.is_empty() {
60			BudgetAllocation::<T>::put(B::get());
61			weight = weight.saturating_add(T::DbWeight::get().writes(1));
62			log::info!(target: LOG_TARGET, "Initialized BudgetAllocation with default budget");
63		}
64
65		let now: u64 = T::Time::now().saturated_into();
66
67		// Only inflate if `LastIssuanceTimestamp` not set.
68		if !LastIssuanceTimestamp::<T>::get().is_zero() {
69			log::warn!(
70				target: LOG_TARGET,
71				"DAP V1->V2: LastIssuanceTimestamp already set; skipping catch-up drip"
72			);
73			return weight;
74		}
75
76		let last_inflation = P::get();
77		let raw_elapsed = now.saturating_sub(last_inflation);
78		let elapsed = raw_elapsed.min(M::get());
79		if elapsed < raw_elapsed {
80			log::info!(
81				target: LOG_TARGET,
82				"DAP V1->V2: elapsed {raw_elapsed}ms clamped to bound {elapsed}ms"
83			);
84		}
85		let minted = pallet::Pallet::<T>::mint_and_distribute(elapsed);
86		weight = weight.saturating_add(<T as Config>::WeightInfo::drip_issuance());
87
88		// Regular drips resume from `now`.
89		LastIssuanceTimestamp::<T>::put(now);
90		weight = weight.saturating_add(T::DbWeight::get().writes(1));
91
92		log::info!(
93			target: LOG_TARGET,
94			"DAP V1->V2: elapsed={elapsed}ms, total_minted={minted:?}, \
95			 seeded LastIssuanceTimestamp={now}"
96		);
97
98		weight
99	}
100
101	#[cfg(feature = "try-runtime")]
102	fn pre_upgrade() -> Result<alloc::vec::Vec<u8>, sp_runtime::TryRuntimeError> {
103		use codec::Encode;
104
105		frame_support::ensure!(
106			LastIssuanceTimestamp::<T>::get() == 0 || BudgetAllocation::<T>::get().is_empty(),
107			"Migration not needed: LastIssuanceTimestamp and BudgetAllocation already set"
108		);
109
110		// Capture `now` and the expected catch-up mint to validate post-upgrade.
111		let last_inflation = P::get();
112		let now: u64 = T::Time::now().saturated_into();
113		let elapsed = now.saturating_sub(last_inflation).min(M::get());
114		let total_issuance_before = T::Currency::total_issuance();
115		let expected_mint = T::IssuanceCurve::issue(total_issuance_before, elapsed);
116
117		Ok((now, total_issuance_before, expected_mint).encode())
118	}
119
120	#[cfg(feature = "try-runtime")]
121	fn post_upgrade(state: alloc::vec::Vec<u8>) -> Result<(), sp_runtime::TryRuntimeError> {
122		use codec::Decode;
123
124		let (expected_now, total_issuance_before, expected_mint) =
125			<(u64, BalanceOf<T>, BalanceOf<T>)>::decode(&mut &state[..])
126				.map_err(|_| "pre_upgrade state decode failed")?;
127
128		// Clock hand-off: regular drips resume from `now`.
129		frame_support::ensure!(
130			LastIssuanceTimestamp::<T>::get() == expected_now,
131			"LastIssuanceTimestamp should equal `now` after migration"
132		);
133
134		// Budget invariants (non-empty, registered keys, sum == 100%).
135		pallet::Pallet::<T>::do_try_state()?;
136
137		// The catch-up mint should land in `[expected - dust, expected]`. Each recipient
138		// share is `Perbill::mul_floor(issuance)`, so the sum only ever rounds *down*,
139		// bounded by one unit per budget entry. Anything outside this window indicates
140		// something other than the catch-up touched issuance.
141		let actual_mint = T::Currency::total_issuance().saturating_sub(total_issuance_before);
142		let budget_len = BudgetAllocation::<T>::get().len();
143		let max_dust = BalanceOf::<T>::from(budget_len as u32);
144		frame_support::ensure!(
145			actual_mint <= expected_mint && actual_mint.saturating_add(max_dust) >= expected_mint,
146			"Catch-up mint outside expected [expected - dust, expected] window"
147		);
148
149		Ok(())
150	}
151}