1use alloc::collections::btree_map::BTreeMap;
38#[cfg(feature = "try-runtime")]
39use alloc::vec::Vec;
40use frame_support::{
41 pallet_prelude::{Get, Weight},
42 traits::{
43 fungible::metadata::Inspect as FungibleMetadataInspect,
44 fungibles::metadata::Inspect as FungiblesMetadataInspect,
45 },
46};
47use sp_runtime::Permill;
48
49use crate::{
50 pallet::{
51 AssetCeilingWeight, CircuitBreakerLevel, ExternalAssets, ExternalDecimals,
52 InternalDecimals, MaxPsmDebtOfTotal, MintingFee, RedemptionFee, MAX_DECIMALS_DIFF,
53 },
54 Config, Pallet,
55};
56
57#[cfg(feature = "try-runtime")]
58use frame_support::ensure;
59#[cfg(feature = "try-runtime")]
60use sp_runtime::TryRuntimeError;
61
62const LOG_TARGET: &str = "runtime::psm::migration";
63
64pub trait InitialPsmConfig<T: Config> {
69 fn max_psm_debt_of_total() -> Permill;
71
72 fn asset_configs() -> BTreeMap<T::AssetId, (Permill, Permill, Permill)>;
79}
80
81pub struct InitializePsm<T, I>(core::marker::PhantomData<(T, I)>);
91
92impl<T: Config, I: InitialPsmConfig<T>> frame_support::traits::OnRuntimeUpgrade
93 for InitializePsm<T, I>
94{
95 fn on_runtime_upgrade() -> Weight {
96 log::info!(
97 target: LOG_TARGET,
98 "Running InitializePsm: initializing PSM pallet parameters"
99 );
100
101 let asset_configs = I::asset_configs();
102 let mut reads = 0u64;
103 let mut writes = 0u64;
104
105 reads += 1;
106 if !MaxPsmDebtOfTotal::<T>::exists() {
107 MaxPsmDebtOfTotal::<T>::put(I::max_psm_debt_of_total());
108 writes += 1;
109 }
110
111 let internal_decimals = T::InternalAsset::decimals();
116 reads += 1;
117 if !InternalDecimals::<T>::exists() {
118 InternalDecimals::<T>::put(internal_decimals);
119 writes += 1;
120 }
121 for (asset_id, (minting_fee, redemption_fee, ceiling_weight)) in &asset_configs {
122 reads += 1;
123 if ExternalAssets::<T>::contains_key(asset_id) {
125 log::info!(
126 target: LOG_TARGET,
127 "Asset {:?} already configured, skipping",
128 asset_id,
129 );
130 continue;
131 }
132
133 let asset_decimals = T::Fungibles::decimals(asset_id.clone());
134 let diff = asset_decimals.abs_diff(internal_decimals) as u32;
135 if diff > MAX_DECIMALS_DIFF {
136 log::error!(
137 target: LOG_TARGET,
138 "Asset {:?} decimals diff ({}) exceeds MAX_DECIMALS_DIFF ({}), skipping",
139 asset_id,
140 diff,
141 MAX_DECIMALS_DIFF,
142 );
143 continue;
144 }
145
146 ExternalAssets::<T>::insert(asset_id, CircuitBreakerLevel::AllEnabled);
147 ExternalDecimals::<T>::insert(asset_id, asset_decimals);
148 MintingFee::<T>::insert(asset_id, minting_fee);
149 RedemptionFee::<T>::insert(asset_id, redemption_fee);
150 AssetCeilingWeight::<T>::insert(asset_id, ceiling_weight);
151 writes += 5;
152
153 log::info!(
154 target: LOG_TARGET,
155 "Configured external asset {:?} (decimals={})",
156 asset_id,
157 asset_decimals,
158 );
159 }
160
161 Pallet::<T>::ensure_account_exists(&Pallet::<T>::account_id());
162 Pallet::<T>::ensure_account_exists(&T::FeeDestination::get());
163 writes += 2;
164
165 log::info!(
166 target: LOG_TARGET,
167 "InitializePsm complete"
168 );
169
170 T::DbWeight::get().reads_writes(reads, writes)
171 }
172
173 #[cfg(feature = "try-runtime")]
174 fn pre_upgrade() -> Result<Vec<u8>, TryRuntimeError> {
175 Ok(Vec::new())
176 }
177
178 #[cfg(feature = "try-runtime")]
179 fn post_upgrade(_state: Vec<u8>) -> Result<(), TryRuntimeError> {
180 ensure!(
181 MaxPsmDebtOfTotal::<T>::get() == I::max_psm_debt_of_total(),
182 "MaxPsmDebtOfTotal mismatch after migration"
183 );
184
185 for (asset_id, (minting_fee, redemption_fee, ceiling_weight)) in I::asset_configs() {
186 ensure!(
187 ExternalAssets::<T>::get(&asset_id) == Some(CircuitBreakerLevel::AllEnabled),
188 "External asset missing or not AllEnabled after migration"
189 );
190 ensure!(
191 MintingFee::<T>::get(&asset_id) == minting_fee,
192 "MintingFee mismatch after migration"
193 );
194 ensure!(
195 RedemptionFee::<T>::get(&asset_id) == redemption_fee,
196 "RedemptionFee mismatch after migration"
197 );
198 ensure!(
199 AssetCeilingWeight::<T>::get(&asset_id) == ceiling_weight,
200 "AssetCeilingWeight mismatch after migration"
201 );
202 }
203
204 let psm_account = Pallet::<T>::account_id();
205 ensure!(
206 frame_system::Pallet::<T>::account_exists(&psm_account),
207 "PSM account does not exist after migration"
208 );
209
210 Ok(())
211 }
212}
213
214#[cfg(test)]
215mod tests {
216 use super::*;
217 use crate::mock::{new_test_ext, Assets, Test, ALICE, USDC_ASSET_ID, USDT_ASSET_ID};
218 use frame_support::assert_ok;
219
220 struct TestPsmConfig;
221
222 impl InitialPsmConfig<Test> for TestPsmConfig {
223 fn max_psm_debt_of_total() -> Permill {
224 Permill::from_percent(25)
225 }
226
227 fn asset_configs() -> BTreeMap<u32, (Permill, Permill, Permill)> {
228 [
229 (
230 USDC_ASSET_ID,
231 (
232 Permill::from_parts(5_000),
233 Permill::from_parts(5_000),
234 Permill::from_percent(50),
235 ),
236 ),
237 (
238 USDT_ASSET_ID,
239 (
240 Permill::from_parts(3_000),
241 Permill::from_parts(7_000),
242 Permill::from_percent(50),
243 ),
244 ),
245 ]
246 .into_iter()
247 .collect()
248 }
249 }
250
251 fn clear_all_psm_state() {
252 MaxPsmDebtOfTotal::<Test>::kill();
253 InternalDecimals::<Test>::kill();
254 ExternalAssets::<Test>::remove(USDC_ASSET_ID);
255 ExternalAssets::<Test>::remove(USDT_ASSET_ID);
256 MintingFee::<Test>::remove(USDC_ASSET_ID);
257 MintingFee::<Test>::remove(USDT_ASSET_ID);
258 RedemptionFee::<Test>::remove(USDC_ASSET_ID);
259 RedemptionFee::<Test>::remove(USDT_ASSET_ID);
260 AssetCeilingWeight::<Test>::remove(USDC_ASSET_ID);
261 AssetCeilingWeight::<Test>::remove(USDT_ASSET_ID);
262 ExternalDecimals::<Test>::remove(USDC_ASSET_ID);
263 ExternalDecimals::<Test>::remove(USDT_ASSET_ID);
264 }
265
266 #[test]
267 fn initialize_psm_configures_new_assets() {
268 use frame_support::traits::{
269 fungible::metadata::Inspect as _, fungibles::metadata::Inspect as _, OnRuntimeUpgrade,
270 };
271
272 new_test_ext().execute_with(|| {
273 clear_all_psm_state();
274
275 InitializePsm::<Test, TestPsmConfig>::on_runtime_upgrade();
276
277 assert_eq!(MaxPsmDebtOfTotal::<Test>::get(), TestPsmConfig::max_psm_debt_of_total());
278 assert_eq!(
279 InternalDecimals::<Test>::get(),
280 Some(<Test as Config>::InternalAsset::decimals())
281 );
282
283 for (asset_id, (minting_fee, redemption_fee, ceiling_weight)) in
284 TestPsmConfig::asset_configs()
285 {
286 assert_eq!(
287 ExternalAssets::<Test>::get(asset_id),
288 Some(CircuitBreakerLevel::AllEnabled)
289 );
290 assert_eq!(
292 ExternalDecimals::<Test>::get(asset_id),
293 Some(<Test as Config>::Fungibles::decimals(asset_id))
294 );
295 assert_eq!(MintingFee::<Test>::get(asset_id), minting_fee);
296 assert_eq!(RedemptionFee::<Test>::get(asset_id), redemption_fee);
297 assert_eq!(AssetCeilingWeight::<Test>::get(asset_id), ceiling_weight);
298 }
299 });
300 }
301
302 #[test]
303 fn initialize_psm_populates_internal_decimals_when_missing() {
304 use frame_support::traits::{fungible::metadata::Inspect as _, OnRuntimeUpgrade};
305
306 new_test_ext().execute_with(|| {
307 InternalDecimals::<Test>::kill();
310
311 InitializePsm::<Test, TestPsmConfig>::on_runtime_upgrade();
312
313 assert_eq!(
314 InternalDecimals::<Test>::get(),
315 Some(<Test as Config>::InternalAsset::decimals())
316 );
317 });
318 }
319
320 #[test]
321 fn initialize_psm_preserves_existing_internal_decimals() {
322 use frame_support::traits::OnRuntimeUpgrade;
323
324 new_test_ext().execute_with(|| {
325 InternalDecimals::<Test>::put(42u8);
327
328 InitializePsm::<Test, TestPsmConfig>::on_runtime_upgrade();
329
330 assert_eq!(InternalDecimals::<Test>::get(), Some(42));
331 });
332 }
333
334 #[test]
335 fn initialize_psm_skips_existing_assets() {
336 use frame_support::traits::OnRuntimeUpgrade;
337
338 new_test_ext().execute_with(|| {
339 ExternalAssets::<Test>::insert(USDC_ASSET_ID, CircuitBreakerLevel::MintingDisabled);
343 MintingFee::<Test>::insert(USDC_ASSET_ID, Permill::from_percent(10));
344 ExternalDecimals::<Test>::remove(USDC_ASSET_ID);
345
346 ExternalAssets::<Test>::remove(USDT_ASSET_ID);
348 MintingFee::<Test>::remove(USDT_ASSET_ID);
349 RedemptionFee::<Test>::remove(USDT_ASSET_ID);
350 AssetCeilingWeight::<Test>::remove(USDT_ASSET_ID);
351 ExternalDecimals::<Test>::remove(USDT_ASSET_ID);
352
353 InitializePsm::<Test, TestPsmConfig>::on_runtime_upgrade();
354
355 assert_eq!(
357 ExternalAssets::<Test>::get(USDC_ASSET_ID),
358 Some(CircuitBreakerLevel::MintingDisabled)
359 );
360 assert_eq!(MintingFee::<Test>::get(USDC_ASSET_ID), Permill::from_percent(10));
361 assert_eq!(ExternalDecimals::<Test>::get(USDC_ASSET_ID), None);
362
363 let (_, (minting_fee, redemption_fee, ceiling_weight)) = TestPsmConfig::asset_configs()
365 .into_iter()
366 .find(|(id, _)| *id == USDT_ASSET_ID)
367 .unwrap();
368 assert_eq!(
369 ExternalAssets::<Test>::get(USDT_ASSET_ID),
370 Some(CircuitBreakerLevel::AllEnabled)
371 );
372 assert!(ExternalDecimals::<Test>::get(USDT_ASSET_ID).is_some());
373 assert_eq!(MintingFee::<Test>::get(USDT_ASSET_ID), minting_fee);
374 assert_eq!(RedemptionFee::<Test>::get(USDT_ASSET_ID), redemption_fee);
375 assert_eq!(AssetCeilingWeight::<Test>::get(USDT_ASSET_ID), ceiling_weight);
376 });
377 }
378
379 #[test]
380 fn initialize_psm_skips_assets_with_wrong_decimals() {
381 use frame_support::traits::{
382 fungibles::{metadata::Mutate as MetadataMutate, Create as FungiblesCreate},
383 OnRuntimeUpgrade,
384 };
385
386 const WRONG_DECIMALS_ID: u32 = 99;
387
388 new_test_ext().execute_with(|| {
389 assert_ok!(<Assets as FungiblesCreate<u64>>::create(WRONG_DECIMALS_ID, ALICE, true, 1));
391 assert_ok!(<Assets as MetadataMutate<u64>>::set(
392 WRONG_DECIMALS_ID,
393 &ALICE,
394 b"Wrong".to_vec(),
395 b"WRG".to_vec(),
396 (MAX_DECIMALS_DIFF + 6 + 1).try_into().unwrap(), ));
398
399 struct MixedDecimalsConfig;
400 impl InitialPsmConfig<Test> for MixedDecimalsConfig {
401 fn max_psm_debt_of_total() -> Permill {
402 Permill::from_percent(50)
403 }
404 fn asset_configs() -> BTreeMap<u32, (Permill, Permill, Permill)> {
405 [
406 (
407 WRONG_DECIMALS_ID,
408 (Permill::zero(), Permill::zero(), Permill::from_percent(50)),
409 ),
410 (
411 USDC_ASSET_ID, (Permill::zero(), Permill::zero(), Permill::from_percent(50)),
413 ),
414 ]
415 .into_iter()
416 .collect()
417 }
418 }
419
420 ExternalAssets::<Test>::remove(WRONG_DECIMALS_ID);
421 ExternalAssets::<Test>::remove(USDC_ASSET_ID);
422
423 InitializePsm::<Test, MixedDecimalsConfig>::on_runtime_upgrade();
424
425 assert_eq!(ExternalAssets::<Test>::get(WRONG_DECIMALS_ID), None);
427
428 assert_eq!(
430 ExternalAssets::<Test>::get(USDC_ASSET_ID),
431 Some(CircuitBreakerLevel::AllEnabled)
432 );
433 });
434 }
435
436 #[test]
437 fn initialize_psm_is_idempotent() {
438 use frame_support::traits::OnRuntimeUpgrade;
439
440 new_test_ext().execute_with(|| {
441 clear_all_psm_state();
442
443 InitializePsm::<Test, TestPsmConfig>::on_runtime_upgrade();
445 InitializePsm::<Test, TestPsmConfig>::on_runtime_upgrade();
446
447 assert_eq!(MaxPsmDebtOfTotal::<Test>::get(), TestPsmConfig::max_psm_debt_of_total());
449 for (asset_id, _) in TestPsmConfig::asset_configs() {
450 assert_eq!(
451 ExternalAssets::<Test>::get(asset_id),
452 Some(CircuitBreakerLevel::AllEnabled)
453 );
454 assert!(ExternalDecimals::<Test>::get(asset_id).is_some());
455 }
456 });
457 }
458}